Autonomous content creation, review, and multi-platform publishing for ACT ecosystem...
Empathy Ledger serves as the central content hub for the ACT ecosystem - photography, videography, writing, and storytelling with intelligent tagging and API-driven syndication.
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā CONTENT SOURCES ā
ā ⢠Your photography and videography ā
ā ⢠Storyteller contributions ā
ā ⢠ACT project milestones ā
ā ⢠Ralph-generated content ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāā¬āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā EMPATHY LEDGER CONTENT HUB ā
ā ā
ā āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā ā
ā ā MEDIA INTELLIGENCE (Consent-Gated) ā ā
ā ā ⢠Auto-tag objects/scenes in photos (Claude Vision) ā ā
ā ā ⢠Face recognition ā link to storyteller profiles ā ā
ā ā ⢠Theme extraction ā narrative connections ā ā
ā ā ⢠ALMA integration (interventions, evidence) ā ā
ā āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā ā
ā ā
ā āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā ā
ā ā THREE-TIER VISIBILITY ā ā
ā ā ⢠Private: Storyteller only ā ā
ā ā ⢠Community: ACT ecosystem + approved partners ā ā
ā ā ⢠Public: Anyone with attribution ā ā
ā āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā ā
ā ā
ā āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā ā
ā ā CONTENT SYNDICATION API ā ā
ā ā ⢠GET /api/v1/content-hub/stories ā ā
ā ā ⢠GET /api/v1/content-hub/articles ā ā
ā ā ⢠GET /api/v1/content-hub/media/library ā ā
ā ā ⢠GET /api/v1/content-hub/themes ā ā
ā ā ⢠POST /api/v1/content-hub/syndicate ā ā
ā āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāā¬āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā DISTRIBUTION ā
ā ā
ā ACT Projects (via API) External Users Social ā
ā ⢠JusticeHub ⢠Partner orgs ⢠GHL ā
ā ⢠ACT Farm ⢠Researchers ⢠Social ā
ā ⢠The Harvest ⢠Media ⢠Posts ā
ā ⢠Goods on Country ā
ā ⢠ACT Placemat ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
| Table | Purpose |
|---|---|
media_ai_analysis |
Smart photo/video tagging (consent-gated) |
media_person_recognition |
Face recognition with two-party consent |
media_narrative_themes |
Theme extraction and story linking |
articles |
Blog/article content for syndication |
content_syndication |
Track content distribution |
// Photo analysis with consent gate
import { createPhotoAnalyzer } from './lib/media-intelligence/photo-analyzer';
const analyzer = createPhotoAnalyzer();
// 1. Grant consent (REQUIRED before any AI analysis)
await analyzer.grantConsent(mediaAssetId, storytellerId, grantedBy);
// 2. Analyze photo
const result = await analyzer.analyzePhoto({
mediaAssetId,
storytellerId,
imageUrl,
consentGranted: true
});
// Result includes:
// - detectedObjects: [{label, confidence}]
// - sceneClassification: 'portrait' | 'ceremony' | etc.
// - autoTags: ['community', 'gathering', ...]
// - culturalReviewRequired: boolean
// - emotionalTone: 'hopeful' | 'reflective' | etc.
import { createFaceRecognition } from './lib/media-intelligence/face-recognition';
const faces = createFaceRecognition();
// Step 1: Uploader grants consent
await faces.grantUploaderConsent(recognitionId, uploaderId);
// Step 2: Person in photo grants consent
await faces.grantPersonConsent(recognitionId, storytellerId, personId, canBePublic);
// Only after BOTH consents can identity be linked
| Endpoint | Method | Description |
|---|---|---|
/api/v1/content-hub/stories |
GET | List published stories |
/api/v1/content-hub/stories/:id |
GET | Single story with full content |
/api/v1/content-hub/articles |
GET | List published articles |
/api/v1/content-hub/articles/:slug |
GET | Single article by slug |
/api/v1/content-hub/media/library |
GET | Browse media library |
/api/v1/content-hub/themes |
GET | Discover content by themes |
/api/v1/content-hub/search |
GET | Full-text search |
/api/v1/content-hub/syndicate |
POST | Register syndication |
| Need | Reference |
|---|---|
| Database migration | empathy-ledger-v2/supabase/migrations/20260109000001_content_hub_schema.sql |
| Photo analyzer | references/photo-analyzer.ts |
| Face recognition | references/face-recognition.ts |
| Theme extractor | references/theme-extractor.ts |
| Syndication API | references/content-syndication-api.ts |
| Command | Description |
|---|---|
generate <topic> |
Create draft post from ecosystem knowledge |
sync |
Sync all sources (Notion + Empathy Ledger) to GHL |
sync --notion-only |
Sync only Notion content to GHL |
sync --empathy-only |
Sync only Empathy Ledger content to GHL |
status |
Check publishing pipeline status |
schedule <date> |
Schedule content for specific date |
ideas |
Generate post ideas from recent activity |
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā 1. CONTENT SOURCES ā
ā ⢠Ralph PRD completions ā auto-generate posts ā
ā ⢠Sprint milestones ā project updates ā
ā ⢠Codebase commits ā technical insights ā
ā ⢠Brand alignment skill ā voice/tone check ā
ā ⢠Empathy Ledger articles & stories (NEW) ā
āāāāāāāāāāāāāāāāāāāāāāā¬āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā 2A. NOTION CONTENT HUB ā
ā Database: e400e93e-fd9d-4a21-810c-58d67ed9fe97 ā
ā ā
ā Status Flow: ā
ā Story in Development ā Ready to Connect ā ā
ā Conversation Scheduled ā Connected & Delighted ā
āāāāāāāāāāāāāāāāāāāāāāā¬āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā
āāāāāāāāāāāāāāāāāāāāāāā“āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā 2B. EMPATHY LEDGER CONTENT HUB ā
ā URL: EMPATHY_LEDGER_URL (default: localhost:3000) ā
ā ā
ā Status Flow: ā
ā Draft ā In Review ā Elder Review ā Published ā
ā ā
ā Article Types: ā
ā ⢠story_feature, program_spotlight ā
ā ⢠research_summary, community_news ā
ā ⢠editorial, impact_report ā
ā ā
ā Stories with syndication_enabled = true ā
āāāāāāāāāāāāāāāāāāāāāāā¬āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā 3. GHL PUBLISHING ā
ā Script: scripts/sync-content-to-ghl.mjs ā
ā ā
ā Syncs from BOTH sources automatically: ā
ā ⢠Notion "Ready to Connect" items ā
ā ⢠Empathy Ledger published articles/stories ā
ā ā
ā Connected Accounts: ā
ā ⢠LinkedIn (Company) - A Curious Tractor ā
ā ⢠LinkedIn (Personal) - Benjamin Knight ā
ā ⢠YouTube - A Curious Tractor ā
ā ⢠Google Business Profile ā
ā ⢠Bluesky - A Curious Tractor ā
ā ā
ā View engagement: GHL Dashboard ā Social Planner ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
| Account | Env Variable | Best For |
|---|---|---|
| LinkedIn (Company) | GHL_LINKEDIN_ACCOUNT_ID |
Professional updates, milestones |
| LinkedIn (Personal) | GHL_LINKEDIN_PERSONAL_ID |
Personal stories, reflections |
| YouTube | GHL_YOUTUBE_ACCOUNT_ID |
Video announcements |
| Google Business | GHL_GBP_ACCOUNT_ID |
Local updates, events |
| Bluesky | GHL_BLUESKY_ACCOUNT_ID |
Tech community, open source |
When Ralph completes a PRD feature, auto-generate announcement:
// Read ralph/progress.txt for recent completions
// Generate post using act-brand-alignment voice
// Create in Notion with "Story in Development" status
Query sprint tracking for completed items, generate weekly digest.
Scan all 7 project codebases for:
node scripts/sync-content-to-ghl.mjs
node scripts/sync-content-to-ghl.mjs --notion-only
node scripts/sync-content-to-ghl.mjs --empathy-only
node scripts/sync-content-to-ghl.mjs --dry-run
node scripts/sync-content-to-ghl.mjs --status
node scripts/check-ghl-engagement.mjs
Always invoke act-brand-alignment skill before publishing to ensure:
/data_sources/{id}/query (not /databases/)7005d0d1-41d3-436c-9f86-526d275c2f10e400e93e-fd9d-4a21-810c-58d67ed9fe97āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā 1. AGGREGATE ECOSYSTEM KNOWLEDGE ā
ā node scripts/aggregate-ecosystem-knowledge.mjs ā
ā ā
ā Scans: Git activity, Ralph progress, Notion sprints, ā
ā Documentation updates, Deployments ā
āāāāāāāāāāāāāāāāāāāāāāā¬āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā 2. GENERATE CONTENT ā
ā node scripts/generate-content-from-knowledge.mjs ā
ā ā
ā Types: weekly-activity, feature-shipped, sprint-milestone, ā
ā technical-insight, project-spotlight ā
ā ā
ā Output: Drafts in Notion with "Story in Development" ā
āāāāāāāāāāāāāāāāāāāāāāā¬āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā 3. HUMAN REVIEW (Notion) ā
ā Edit content, add images, approve ā
ā Change status to "Ready to Connect" ā
āāāāāāāāāāāāāāāāāāāāāāā¬āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā 4. PUBLISH ā
ā node scripts/sync-content-to-ghl.mjs ā
ā ā
ā Auto-syncs approved content to social platforms ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
# Generate content drafts from ecosystem knowledge
node scripts/generate-content-from-knowledge.mjs
# Preview without creating (dry run)
node scripts/generate-content-from-knowledge.mjs --dry-run
# Generate specific type
node scripts/generate-content-from-knowledge.mjs --type weekly-activity
# Sync approved content to social
node scripts/sync-content-to-ghl.mjs
# Check status
node scripts/sync-content-to-ghl.mjs --status
# Ralph content runner (combines all)
./ralph/ralph-content.sh
| Type | Description | Best Accounts |
|---|---|---|
weekly-activity |
Ecosystem commit summary | LinkedIn (Company) |
feature-shipped |
Ralph completion announcement | Both LinkedIn |
sprint-milestone |
Sprint progress update | LinkedIn (Company) |
technical-insight |
Technical learnings | LinkedIn (Personal) |
project-spotlight |
Project deep dive | Both LinkedIn |
| Schedule | Action |
|---|---|
| Sunday 10pm UTC (Monday 8am AEST) | Generate weekly content |
| Every 4 hours | Sync approved content to GHL |
Trigger manually: Actions ā Content Pipeline ā Run workflow
import { createMediaService } from './scripts/lib/media-service.mjs';
const media = createMediaService();
// Get brand asset
const logoUrl = media.getBrandAsset('logo');
// Get project hero image
const heroUrl = media.getProjectAsset('JusticeHub');
// Upload local file
const result = await media.uploadFile('./image.png');
// Suggest image from content
const suggestion = media.suggestImage('Building ecosystem together');
tednluwflfhxyucgwigh.supabase.co/storage/v1/object/public/content-media/brand/ folderprojects/ folderuploads/ folderContent generation auto-suggests images:
For custom images:
| Need | Reference |
|---|---|
| Content generation | scripts/generate-content-from-knowledge.mjs |
| Knowledge aggregator | scripts/aggregate-ecosystem-knowledge.mjs |
| Media service | scripts/lib/media-service.mjs |
| Sync script | scripts/sync-content-to-ghl.mjs |
| GHL social service | scripts/lib/ghl-social-service.mjs |
| Empathy Ledger service | scripts/lib/empathy-ledger-content.mjs |
| Content PRD template | ralph/content-prd-template.json |
| Ralph content runner | ralph/ralph-content.sh |
| GitHub Action | .github/workflows/content-pipeline.yml |
| Notion database IDs | config/notion-database-ids.json |
| Post creation template | references/post-template.mjs |
| Content ideas generator | references/content-ideas.md |
| Variable | Description | Default |
|---|---|---|
EMPATHY_LEDGER_URL |
Empathy Ledger API base URL | http://localhost:3000 |
CONTENT_HUB_API_KEY |
API key for Content Hub access | Auto-generated |
import { createEmpathyLedgerService } from './lib/empathy-ledger-content.mjs';
const el = createEmpathyLedgerService({
baseUrl: 'https://empathyledger.com',
apiKey: process.env.CONTENT_HUB_API_KEY
});
// Get published articles
const articles = await el.getArticles({ type: 'story_feature', limit: 10 });
// Get public stories
const stories = await el.getStories({ visibility: 'public', limit: 20 });
// Get content ready for GHL sync
const content = await el.getContentForGHLSync();
// Returns: { articles: [...], stories: [...], total: N }
// Transform to GHL post format
const ghlPost = el.articleToGHLPost(article, { accountIds: [...] });
// Register syndication
await el.registerSyndication({
contentType: 'article',
contentId: article.id,
destinationType: 'ghl_social',
ghlPostId: 'xxx'
});
// Health check
const health = await el.healthCheck();
// Returns: { healthy: true, baseUrl, articlesAvailable }