@m13v/s4l 1.6.197-rc.7
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.
- package/README.md +143 -0
- package/SKILL.md +342 -0
- package/bin/cli.js +980 -0
- package/bin/cookie-helper.js +315 -0
- package/bin/platform.js +59 -0
- package/bin/scheduler/index.js +12 -0
- package/bin/scheduler/launchd.js +518 -0
- package/browser-agent-configs/all-agents-mcp.json +68 -0
- package/browser-agent-configs/linkedin-agent-mcp.json +16 -0
- package/browser-agent-configs/linkedin-agent.json +17 -0
- package/browser-agent-configs/linkedin-harness-mcp.json +21 -0
- package/browser-agent-configs/reddit-agent-mcp.json +16 -0
- package/browser-agent-configs/reddit-agent.json +17 -0
- package/browser-agent-configs/twitter-harness-mcp.json +18 -0
- package/config.example.json +45 -0
- package/mcp/dist/index.js +4212 -0
- package/mcp/dist/onboarding.js +200 -0
- package/mcp/dist/panel.html +176 -0
- package/mcp/dist/product-link.html +102 -0
- package/mcp/dist/repo.js +222 -0
- package/mcp/dist/runtime.js +1079 -0
- package/mcp/dist/screencast.js +323 -0
- package/mcp/dist/setup.js +545 -0
- package/mcp/dist/telemetry.js +306 -0
- package/mcp/dist/twitterAuth.js +138 -0
- package/mcp/dist/version.js +271 -0
- package/mcp/dist/version.json +4 -0
- package/mcp/install-runtime.mjs +70 -0
- package/mcp/install.mjs +169 -0
- package/mcp/manifest.json +80 -0
- package/mcp/menubar/dashboard_server.py +213 -0
- package/mcp/menubar/s4l_card.py +1314 -0
- package/mcp/menubar/s4l_log_relay.py +179 -0
- package/mcp/menubar/s4l_menubar.py +2439 -0
- package/mcp/menubar/s4l_state.py +891 -0
- package/mcp/package.json +34 -0
- package/mcp/shared/doctor.cjs +437 -0
- package/mcp/shared/onboarding-ledger.cjs +324 -0
- package/mcp-servers/browser-harness/server.py +968 -0
- package/package.json +160 -0
- package/requirements.txt +20 -0
- package/scripts/_compute_allowlist.py +58 -0
- package/scripts/_db_update.py +20 -0
- package/scripts/_filt.py +9 -0
- package/scripts/_li_notif_match.py +76 -0
- package/scripts/_li_notif_orchestrate.py +126 -0
- package/scripts/_lock_preempt_test.py +60 -0
- package/scripts/_run_icp_precheck.py +57 -0
- package/scripts/a16z_pearx_calendar_reminders.py +99 -0
- package/scripts/account_resolver.py +141 -0
- package/scripts/active_campaigns.py +114 -0
- package/scripts/active_users.py +190 -0
- package/scripts/amplitude_24h_signups.py +468 -0
- package/scripts/amplitude_signups.py +177 -0
- package/scripts/apply_onboarding_selections.py +131 -0
- package/scripts/audience_pages.py +243 -0
- package/scripts/audit_helper.py +120 -0
- package/scripts/author_history_block.py +353 -0
- package/scripts/autopilot_stall_watch.py +284 -0
- package/scripts/backfill_twitter_attempts_topic.py +81 -0
- package/scripts/backfill_twitter_log_post_no_id.py +322 -0
- package/scripts/bench_dashboard.sh +138 -0
- package/scripts/bh_send.py +39 -0
- package/scripts/build_persona.py +409 -0
- package/scripts/bulk_icp.py +18 -0
- package/scripts/campaign_bump.py +51 -0
- package/scripts/capture_thread_media.py +288 -0
- package/scripts/check_browser_lock_health.sh +81 -0
- package/scripts/check_external_pool_depth.py +253 -0
- package/scripts/check_unread_web_chats.py +28 -0
- package/scripts/claim_web_chat.py +47 -0
- package/scripts/classify_run_error.py +158 -0
- package/scripts/claude_job.py +988 -0
- package/scripts/clean_stale_singleton.sh +56 -0
- package/scripts/cleanup_harness_tabs.py +68 -0
- package/scripts/copy_browser_cookies.py +454 -0
- package/scripts/counterparty_history.py +350 -0
- package/scripts/db.py +57 -0
- package/scripts/discover_claude_profiles.py +120 -0
- package/scripts/discover_linkedin_candidates.py +984 -0
- package/scripts/dm_conversation.py +682 -0
- package/scripts/dm_db_update.py +69 -0
- package/scripts/dm_engage_helper.py +161 -0
- package/scripts/dm_outreach_helper.py +147 -0
- package/scripts/dm_outreach_twitter_helper.py +129 -0
- package/scripts/dm_send_log.py +106 -0
- package/scripts/dm_short_links.py +1084 -0
- package/scripts/dump_web_chat_history.py +47 -0
- package/scripts/engage_github.py +640 -0
- package/scripts/engage_reddit.py +1235 -0
- package/scripts/engage_twitter_helper.py +301 -0
- package/scripts/engagement_styles.py +1787 -0
- package/scripts/enrich_twitter_candidates.py +82 -0
- package/scripts/feedback_digest.py +448 -0
- package/scripts/fetch_prospect_profile.py +312 -0
- package/scripts/fetch_twitter_t1.py +134 -0
- package/scripts/find_threads.py +530 -0
- package/scripts/follow_gate_log.py +59 -0
- package/scripts/funnel_per_day.py +194 -0
- package/scripts/generate_daily_human_style.py +494 -0
- package/scripts/generation_trace.py +173 -0
- package/scripts/get_run_cost.py +107 -0
- package/scripts/github_engage_helper.py +93 -0
- package/scripts/github_tools.py +509 -0
- package/scripts/harness_overlay.py +556 -0
- package/scripts/harvest_twitter_following.py +243 -0
- package/scripts/heartbeat.sh +70 -0
- package/scripts/history_context.py +284 -0
- package/scripts/http_api.py +206 -0
- package/scripts/human_dm_replies_helper.py +169 -0
- package/scripts/identity.py +302 -0
- package/scripts/ig_batch_creator.sh +93 -0
- package/scripts/ig_post_type_picker.py +243 -0
- package/scripts/ig_scrape_transcribe.sh +91 -0
- package/scripts/ingest_human_dm_replies.py +271 -0
- package/scripts/ingest_web_chat_replies.py +229 -0
- package/scripts/install_fleet.py +187 -0
- package/scripts/invent_mcp_server.py +350 -0
- package/scripts/invent_topics.py +1462 -0
- package/scripts/learned_preferences.py +263 -0
- package/scripts/li_discovery.py +161 -0
- package/scripts/link_edit_helper.py +142 -0
- package/scripts/link_tail.py +592 -0
- package/scripts/linkedin_api.py +561 -0
- package/scripts/linkedin_browser.py +730 -0
- package/scripts/linkedin_cooldown.py +128 -0
- package/scripts/linkedin_exclusions.py +234 -0
- package/scripts/linkedin_killswitch.py +1333 -0
- package/scripts/linkedin_search_topic_schema.py +49 -0
- package/scripts/linkedin_unipile.py +658 -0
- package/scripts/linkedin_url.py +228 -0
- package/scripts/log_claude_session.py +636 -0
- package/scripts/log_draft.py +143 -0
- package/scripts/log_linkedin_search_attempts.py +126 -0
- package/scripts/log_post.py +651 -0
- package/scripts/log_run.py +364 -0
- package/scripts/log_thread_media.py +108 -0
- package/scripts/log_twitter_search_attempts.py +150 -0
- package/scripts/log_twitter_skips.py +211 -0
- package/scripts/lookup_post.py +78 -0
- package/scripts/mark_web_chat_processed.py +32 -0
- package/scripts/mcp_lock_proxy.py +370 -0
- package/scripts/memory_snapshot.py +972 -0
- package/scripts/merge_review_queue.py +215 -0
- package/scripts/mint_external_pool.py +182 -0
- package/scripts/mint_kent_pool.py +249 -0
- package/scripts/moltbook_post.py +320 -0
- package/scripts/moltbook_tools.py +159 -0
- package/scripts/pending_threads.py +188 -0
- package/scripts/pick_ig_account.py +177 -0
- package/scripts/pick_project.py +208 -0
- package/scripts/pick_search_topic.py +771 -0
- package/scripts/pick_thread_target.py +279 -0
- package/scripts/pick_twitter_thread_target.py +202 -0
- package/scripts/podlog_fetch_batch.sh +32 -0
- package/scripts/post_github.py +1311 -0
- package/scripts/post_reddit.py +2668 -0
- package/scripts/precompute_dashboard_stats.py +204 -0
- package/scripts/preflight.sh +297 -0
- package/scripts/progress.py +88 -0
- package/scripts/project_excludes.py +353 -0
- package/scripts/project_slugs.py +91 -0
- package/scripts/project_stats.py +241 -0
- package/scripts/project_stats_json.py +1563 -0
- package/scripts/project_topics.py +192 -0
- package/scripts/qualified_query_bank.py +436 -0
- package/scripts/reap_stale_claude_sessions.py +867 -0
- package/scripts/reddit_browser.py +2549 -0
- package/scripts/reddit_browser_fetch.py +141 -0
- package/scripts/reddit_browser_lock.py +593 -0
- package/scripts/reddit_chat_sync.py +710 -0
- package/scripts/reddit_query_bank.py +200 -0
- package/scripts/reddit_threads_helper.py +151 -0
- package/scripts/reddit_tools.py +956 -0
- package/scripts/refresh_instagram_tokens.py +280 -0
- package/scripts/release-mcpb.sh +497 -0
- package/scripts/reply_db.py +334 -0
- package/scripts/reply_insert.py +98 -0
- package/scripts/reply_risk_digest.py +761 -0
- package/scripts/reset-test-machine.sh +602 -0
- package/scripts/restore_twitter_session.py +177 -0
- package/scripts/ripen_reddit_plan.py +478 -0
- package/scripts/run_claude.sh +433 -0
- package/scripts/run_moltbook_cycle.py +555 -0
- package/scripts/s4l_box_update.sh +226 -0
- package/scripts/s4l_channel.py +103 -0
- package/scripts/s4l_ctl.sh +75 -0
- package/scripts/s4l_env.py +47 -0
- package/scripts/saps_activity.py +126 -0
- package/scripts/saps_mode.py +328 -0
- package/scripts/scan_dm_candidates.py +580 -0
- package/scripts/scan_github_replies.py +168 -0
- package/scripts/scan_instagram_comments.py +481 -0
- package/scripts/scan_moltbook_replies.py +252 -0
- package/scripts/scan_pii.py +190 -0
- package/scripts/scan_reddit_replies.py +377 -0
- package/scripts/scan_twitter_mentions_browser.py +327 -0
- package/scripts/scan_twitter_thread_followups.py +299 -0
- package/scripts/scan_x_profile.py +384 -0
- package/scripts/schedule_state.py +202 -0
- package/scripts/scheduled_tasks_snapshot.py +123 -0
- package/scripts/score_linkedin_candidates.py +419 -0
- package/scripts/score_twitter_candidates.py +718 -0
- package/scripts/scrape_linkedin_comment_stats.py +1755 -0
- package/scripts/scrape_linkedin_stats_browser.py +52 -0
- package/scripts/scrape_reddit_views.py +365 -0
- package/scripts/seed_search_queries.py +453 -0
- package/scripts/seed_search_topics.py +127 -0
- package/scripts/send_web_chat_reply.py +130 -0
- package/scripts/sentry_init.py +128 -0
- package/scripts/setup_twitter_auth.py +1320 -0
- package/scripts/snapshot.py +583 -0
- package/scripts/stats.py +2702 -0
- package/scripts/stats_helper.py +52 -0
- package/scripts/strike_alert.py +783 -0
- package/scripts/sweep_post_link_clicks.py +107 -0
- package/scripts/sync_ig_to_posts.py +147 -0
- package/scripts/test_browser_lock.py +189 -0
- package/scripts/test_installation_api.sh +52 -0
- package/scripts/test_percard_posting.py +142 -0
- package/scripts/top_dud_linkedin_queries.py +71 -0
- package/scripts/top_dud_reddit_queries.py +67 -0
- package/scripts/top_dud_twitter_queries.py +71 -0
- package/scripts/top_dud_twitter_topics.py +102 -0
- package/scripts/top_linkedin_queries.py +55 -0
- package/scripts/top_omitted_reddit_topics.py +91 -0
- package/scripts/top_performers.py +588 -0
- package/scripts/top_search_topics.py +180 -0
- package/scripts/top_twitter_queries.py +190 -0
- package/scripts/twitter_access_check.py +382 -0
- package/scripts/twitter_account.py +41 -0
- package/scripts/twitter_batch_phase.py +126 -0
- package/scripts/twitter_browser.py +2804 -0
- package/scripts/twitter_cookie_mirror.py +130 -0
- package/scripts/twitter_cycle_helper.py +310 -0
- package/scripts/twitter_gen_links.py +287 -0
- package/scripts/twitter_post_plan.py +1188 -0
- package/scripts/twitter_scan.py +324 -0
- package/scripts/twitter_supply_signal.py +57 -0
- package/scripts/twitter_threads_helper.py +152 -0
- package/scripts/unclaim_web_chat.py +29 -0
- package/scripts/update_instagram_stats.py +261 -0
- package/scripts/update_linkedin_stats_from_feed.py +328 -0
- package/scripts/version.py +72 -0
- package/scripts/watchdog_hung_runs.py +343 -0
- package/scripts/write_generation_trace.py +73 -0
- package/setup/SKILL.md +277 -0
- package/skill/amplitude-24h-signups.sh +38 -0
- package/skill/archive-old-logs.sh +40 -0
- package/skill/audit-dm-staleness.sh +42 -0
- package/skill/audit-linkedin.sh +14 -0
- package/skill/audit-moltbook.sh +4 -0
- package/skill/audit-reddit-resurrect.sh +67 -0
- package/skill/audit-reddit.sh +4 -0
- package/skill/audit-twitter.sh +4 -0
- package/skill/audit.sh +287 -0
- package/skill/backfill-twitter-attempts-topic.sh +19 -0
- package/skill/backfill-twitter-ghost-posts.sh +24 -0
- package/skill/check-external-pool-depth.sh +7 -0
- package/skill/check-web-chats.sh +203 -0
- package/skill/dm-outreach-linkedin.sh +250 -0
- package/skill/dm-outreach-reddit.sh +274 -0
- package/skill/dm-outreach-twitter.sh +265 -0
- package/skill/engage-dm-replies-linkedin.sh +4 -0
- package/skill/engage-dm-replies-reddit.sh +4 -0
- package/skill/engage-dm-replies-twitter.sh +4 -0
- package/skill/engage-dm-replies.sh +1597 -0
- package/skill/engage-linkedin.sh +581 -0
- package/skill/engage-moltbook.sh +36 -0
- package/skill/engage-reddit.sh +146 -0
- package/skill/engage-twitter.sh +467 -0
- package/skill/github-engage.sh +176 -0
- package/skill/ingest-web-chat-replies.sh +38 -0
- package/skill/invent-supply-test.sh +100 -0
- package/skill/invent-topics.sh +50 -0
- package/skill/lib/linkedin-backend.sh +364 -0
- package/skill/lib/platform.sh +48 -0
- package/skill/lib/reddit-backend.sh +234 -0
- package/skill/lib/twitter-backend.sh +314 -0
- package/skill/link-edit-github.sh +136 -0
- package/skill/link-edit-moltbook.sh +117 -0
- package/skill/link-edit-reddit.sh +201 -0
- package/skill/linkedin-presence.sh +182 -0
- package/skill/linkedin-recovery.sh +282 -0
- package/skill/lock.sh +647 -0
- package/skill/memory-snapshot.sh +39 -0
- package/skill/precompute-stats.sh +35 -0
- package/skill/prewarm-funnel.sh +104 -0
- package/skill/refresh-instagram-tokens.sh +57 -0
- package/skill/refresh-twitter-following.sh +52 -0
- package/skill/reply-risk-digest.sh +31 -0
- package/skill/run-cycle-update-guard.sh +44 -0
- package/skill/run-draft-and-publish.sh +123 -0
- package/skill/run-generate-daily-style.sh +50 -0
- package/skill/run-github-launchd.sh +62 -0
- package/skill/run-github.sh +102 -0
- package/skill/run-instagram-daily.sh +149 -0
- package/skill/run-instagram-render.sh +875 -0
- package/skill/run-linkedin-launchd.sh +81 -0
- package/skill/run-linkedin-unipile.sh +130 -0
- package/skill/run-linkedin.sh +1593 -0
- package/skill/run-moltbook-launchd.sh +61 -0
- package/skill/run-moltbook.sh +38 -0
- package/skill/run-overlay-watch.sh +100 -0
- package/skill/run-reddit-search-launchd.sh +64 -0
- package/skill/run-reddit-search.sh +505 -0
- package/skill/run-reddit-threads-double.sh +32 -0
- package/skill/run-reddit-threads.sh +847 -0
- package/skill/run-scan-moltbook-replies.sh +57 -0
- package/skill/run-twitter-cycle-launchd.sh +63 -0
- package/skill/run-twitter-cycle-singleton.sh +62 -0
- package/skill/run-twitter-cycle.sh +2408 -0
- package/skill/run-twitter-threads.sh +592 -0
- package/skill/scan-instagram-replies.sh +61 -0
- package/skill/scan-twitter-followups.sh +57 -0
- package/skill/social-autoposter-update.sh +66 -0
- package/skill/stats-instagram.sh +72 -0
- package/skill/stats-linkedin.sh +271 -0
- package/skill/stats-moltbook.sh +4 -0
- package/skill/stats-reddit.sh +4 -0
- package/skill/stats-twitter.sh +4 -0
- package/skill/stats.sh +521 -0
- package/skill/strike-alert.sh +18 -0
- package/skill/styles.sh +87 -0
- package/skill/sweep-link-clicks.sh +40 -0
- 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
|