Automated social media posting system that posts one or several photos per day from a specific Flickr album to Instagram with AI-generated captions. The online workflow is managed in GitHub Actions, the captions are generated by OpenAI GPT Vision. Supports multiple Instagram accounts running independently from different Flickr sources.
Instagram's API requires source photos to be published online. I chose my Flickr gallery as the source: https://flickr.com/photos/schaerer/albums/.
The ID '72177720326826937' of a Flickr album (found in the URL; for example, https://flickr.com/photos/schaerer/albums/72177720326826937) serves as the source configuration key.
I take lots of photos that I edit in Adobe Lightroom and publish on my travel blogs, https://travelmemo.com in English and https://reisememo.ch in German. However, I don't take the time to post them on Instagram. This automation helps me repurpose my photos.
- 📅 Daily Posting: Posts one photo per day until the album is complete
- 🎯 Single Album Focus: Processes one specific Flickr album per Instagram account
- 🏢 Multi-Account Support: Independent automation for primary and secondary accounts
- 🌐 Multi-Language AI Captions: German and English caption generation with cultural awareness
- 🎨 Account-Specific Branding: Custom signatures and messaging per account
- 📊 Git-Based State Management: Secure, scalable state storage using Git files with fine-grained PAT permissions
- 🔄 Reusable Workflows: Centralized GitHub Actions workflow eliminates code duplication across accounts
- 🔧 Manual Control: Run automation manually with different options for each account
- 🛡️ Smart Stopping: Automatically stops when all photos are posted
- 📈 Enhanced Blog Integration: WordPress authentication for full editorial content access
- 🔗 EXIF URL Prioritization: Intelligent source URL tracking and content matching
- 🔄 Retry Logic: Automatically retries failed posts and validates image URLs
- 🔀 Independent State: Each account maintains separate posting progress and failed photo tracking
- 📝 Rich Content Context: Blog content extraction with keyword matching for engaging captions
- 🎯 Smart Content Scoring: Advanced algorithms for content relevance assessment
- 🧩 Modular Architecture: Clean separation of concerns with testable orchestration modules
- 🔒 Security-First: Fine-grained PAT permissions, encrypted secrets, minimal access principle
The automation system uses a sophisticated multi-account architecture with enhanced content integration and language-aware processing:
| Component | Purpose | Key Features |
|---|---|---|
account_config.py |
Multi-account configuration | Language-specific settings, branding, prompts |
blog_content_extractor.py |
Enhanced content integration | WordPress auth, keyword matching, EXIF URL prioritization |
caption_generator.py |
Multi-language caption generation | German/English prompts, cultural awareness, blog context |
state_manager.py |
Account-isolated state management | Independent progress tracking, enhanced models |
graph TD
A[Account Configuration] --> B[Photo Selection]
B --> C[EXIF URL Extraction]
C --> D[Blog Content Matching]
D --> E[Multi-Language Caption Generation]
E --> F[Account Branding Integration]
F --> G[Instagram Posting]
G --> H[Account-Isolated State Recording]
H --> I[Progress Tracking]
I --> J[Completion Handling]
- 🌐 Multi-Language Support: Native German and English processing with cultural awareness
- 🏢 Account Isolation: Independent configurations and state management per account
- 📝 Rich Content Integration: WordPress authentication for full editorial content access
- 🎯 Smart Content Matching: EXIF URL prioritization and keyword-based relevance scoring
- 🧪 Comprehensive Testing: Multi-level testing covering account configs, content integration, and language processing
- 🔧 Enhanced Maintainability: Clear separation between account logic, content processing, and caption generation
- 📈 Scalable Design: Easy addition of new accounts, languages, and content sources
The system uses account-specific configuration to enable multi-language, multi-brand operations:
# Account-specific initialization
account_config = get_account_config(account_name)
caption_generator = CaptionGenerator(config, account_config)
blog_extractor = BlogContentExtractor(config, account_config)
state_manager = StateManager(config, account_name)This pattern enables:
- Language isolation with account-specific prompts and cultural conventions
- Brand consistency through account-specific signatures and messaging
- Independent operation with isolated state management per account
The system uses a centralized reusable workflow approach that eliminates code duplication:
graph TB
subgraph "Reusable Workflow"
RW[📋 social-automation.yml<br/>Centralized Logic<br/>Parameterized Inputs]
end
subgraph "Account Workflows"
PW[🏠 primary-flickr-to-insta.yml<br/>Account-Specific Config]
SW[🌍 secondary-flickr-to-insta.yml<br/>Account-Specific Config]
end
PW --> RW
SW --> RW
classDef reusable fill:#e3f2fd,stroke:#0d47a1,stroke-width:2px
classDef account fill:#e8f5e8,stroke:#1b5e20,stroke-width:2px
class RW reusable
class PW,SW account
Modern, secure state storage using Git files instead of repository variables:
- Security: Uses fine-grained PAT permissions (
contents:writeonly) - Scalability: No limits on state data size or complexity
- Auditability: Full Git history of state changes
- Reliability: Atomic operations with conflict resolution
The modular architecture enables comprehensive testing at multiple levels:
- Unit Tests: Individual orchestrator modules (
test_suite/test_orchestration.py) - Integration Tests: End-to-end workflow testing (
test_suite/test_integration.py) - Mock-based Testing: No live API credentials required for most tests
- Fork or clone this repository
- In the file caption_generator.py, adapt the branding to reflect your own brand instead of my Travelmemo brand
- No other code changes are needed - all configuration is done via environment variables
- Optional: To change the publishing schedule, edit the workflow file social-media-automation.yml
Add these repository secrets (Settings > Secrets and variables > Actions > Repository secrets):
FLICKR_API_KEY=your_flickr_api_key
FLICKR_USER_ID=your_flickr_user_id
OPENAI_API_KEY=your_openai_api_key
PERSONAL_ACCESS_TOKEN=your_github_pat_with_contents_write_permission
WORDPRESS_USERNAME=your_wordpress_username
WORDPRESS_APP_PASSWORD=your_wordpress_app_password
# Critical Failure Notification System (REQUIRED for fail-safe automation)
NOTIFICATION_EMAIL=your-alert-email@gmail.com
SMTP_HOST=smtp.gmail.com
SMTP_PORT=587
SMTP_USERNAME=your-email@gmail.com
SMTP_PASSWORD=your_gmail_app_password
Add these repository variables (Settings > Secrets and variables > Actions > Repository variables):
FLICKR_USERNAME=your_flickr_username
GRAPH_API_VERSION=v23.0
OPENAI_MODEL=gpt-4o-mini
Security Note: Use a fine-grained Personal Access Token with only
contents:writepermission instead of the broadreposcope for enhanced security.
Create GitHub environments (Settings > Environments) and configure:
For the main account - primary-account environment:
- Environment variables:
FLICKR_ALBUM_ID,BLOG_POST_URL - Environment secrets:
INSTAGRAM_ACCESS_TOKEN,INSTAGRAM_ACCOUNT_IDFor the secondary account -secondary-accountenvironment: - Environment variables:
FLICKR_ALBUM_ID,BLOG_POST_URL - Environment secrets:
INSTAGRAM_ACCESS_TOKEN,INSTAGRAM_ACCOUNT_ID⚠️ Important: Configure WordPress Application Password for authenticated blog content access.
The fail-safe notification system requires a Gmail App Password for secure email authentication. Never use your regular Gmail password. Step 1: Enable 2-Factor Authentication
- Go to Google Account Security
- Under "Signing in to Google", enable 2-Step Verification
- Complete the 2FA setup process Step 2: Generate App Password
- Go to Google Account Security
- Under "Signing in to Google", click 2-Step Verification
- Scroll down and click App passwords
- Select Mail as the app
- Enter a descriptive name like "Flickr Instagram Automation"
- Click Generate
- Copy the 16-character password (Gmail shows it with spaces:
abcd efgh ijkl mnop) Step 3: Configure GitHub Secrets⚠️ IMPORTANT: Remove all spaces from the app password before storing it. Use the generated app password (not your regular password) for:
SMTP_PASSWORD=abcdefghijklmnop(16 characters, NO SPACES)SMTP_USERNAME=your-email@gmail.com(your Gmail address)NOTIFICATION_EMAIL=your-alert-email@gmail.com(where to receive alerts) App Password Format:- ❌ Wrong:
abcd efgh ijkl mnop(with spaces - will fail authentication) - ✅ Correct:
abcdefghijklmnop(no spaces - required for SMTP) Security Notes: - ✅ App passwords are safer than regular passwords for automation
- ✅ You can revoke app passwords anytime without affecting your account
- ❌ Never store passwords in repository variables (always use secrets)
- ❌ Never use your regular Gmail password for automation
- Go to
Actionstab in your repository - Click on "Flickr to Instagram Automation"
- Click "Run workflow"
- Check "Run without posting (dry run)" for testing
- Click "Run workflow"
The automation follows this simple process:
- Daily Schedule: Runs every day at 9 AM UTC
- Check Album: Fetches all photos from your specified Flickr album
- Find Next Photo: Identifies the next unposted photo
- Generate Caption: Uses GPT-4 Vision to create an engaging caption
- Post to Instagram: Publishes the photo with the generated caption
- Track Progress: Records the post in Repository Variables
- Auto-Complete: Stops automatically when all photos are posted
The system uses environment-specific configuration for multi-account support. No code changes are needed.
For each Instagram account, configure the FLICKR_ALBUM_ID in the respective GitHub environment:
- Main account: Set
FLICKR_ALBUM_IDinprimary-accountenvironment variables - Secondary account: Set
FLICKR_ALBUM_IDinsecondary-accountenvironment variables This allows each account to post from different Flickr albums independently.
Your Flickr album URL looks like:
https://flickr.com/photos/[your_username]/albums/72177720326826937
The album ID is the number at the end: 72177720326826937
The system supports running multiple Instagram accounts independently from the same Flickr source. Each account:
- Has its own workflow and schedule
- Maintains separate posting progress and state
- Uses different Instagram credentials
- Can use different Flickr albums
The secondary account uses environment-specific configuration. Configure the secondary-account environment with:
Environment Variables:
FLICKR_ALBUM_ID=your_secondary_flickr_album_id
BLOG_POST_URL=https://your-secondary-blog.com/your-blog-post-url
Environment Secrets:
INSTAGRAM_ACCESS_TOKEN=your_secondary_instagram_access_token
INSTAGRAM_ACCOUNT_ID=your_secondary_instagram_account_id
Note: The system automatically applies account-specific language settings and branding for the secondary account.
Primary Account:
python main.py --account primary --dry-run
python main.py --account primary --statsSecondary Account:
python main.py --account secondary --dry-run
python main.py --account secondary --stats- Primary Account: Daily at 18:13 UTC (20:13 CEST)
- Secondary Account: Daily at 19:13 UTC (21:13 CEST) Both automations run independently with language-specific processing, account-specific branding, and separate state management.
The automation system follows a modular architecture with clear separation of concerns, robust error handling, and comprehensive state management.
graph TB
%% External Services
subgraph "External APIs"
FA[📷 Flickr API<br/>Photo Retrieval]
IA[📱 Instagram Graph API<br/>Photo Posting]
OA[🤖 OpenAI GPT-4 Vision<br/>Caption Generation]
end
%% GitHub Infrastructure
subgraph "GitHub Infrastructure"
GR[📁 GitHub Repository<br/>Code & Configuration]
GA[⚙️ GitHub Actions<br/>Workflow Engine]
GI[📋 GitHub Storage<br/>State Management]
GS[🔐 GitHub Secrets<br/>Credential Storage]
end
%% Core Application
subgraph "Application Core"
MC[🎮 main.py<br/>Orchestration]
CF[⚙️ config.py<br/>Configuration]
subgraph "API Integrations"
FL[📷 flickr_api.py<br/>Photo Fetching]
CG[🤖 caption_generator.py<br/>AI Caption Creation]
IG[📱 instagram_api.py<br/>Social Media Posting]
end
SM[📊 state_manager.py<br/>Progress Tracking]
end
%% Triggers
subgraph "Triggers"
CR[⏰ Cron Schedule<br/>Daily at 9 AM UTC]
MN[👤 Manual Trigger<br/>Workflow Dispatch]
end
%% Data Flow
CR --> GA
MN --> GA
GA --> MC
MC --> CF
CF --> GS
MC --> FL
FL --> FA
FA --> FL
FL --> CG
CG --> OA
OA --> CG
CG --> IG
IG --> IA
IA --> IG
MC --> SM
SM --> GI
GI --> SM
SM --> MC
%% Styling
classDef external fill:#e1f5fe,stroke:#01579b,stroke-width:2px
classDef github fill:#f3e5f5,stroke:#4a148c,stroke-width:2px
classDef core fill:#e8f5e8,stroke:#1b5e20,stroke-width:2px
classDef trigger fill:#fff3e0,stroke:#e65100,stroke-width:2px
class FA,IA,OA external
class GR,GA,GI,GS github
class MC,CF,FL,CG,IG,SM core
class CR,MN trigger
sequenceDiagram
participant GA as GitHub Actions
participant MC as main.py
participant FL as flickr_api.py
participant FA as Flickr API
participant SM as state_manager.py
participant GI as GitHub Issues
participant CG as caption_generator.py
participant OA as OpenAI API
participant IG as instagram_api.py
participant IA as Instagram API
Note over GA: Daily Trigger (9 AM UTC)
GA->>MC: Execute automation
MC->>SM: Check if album complete
SM->>GI: Query posted photos
GI-->>SM: Return posted photo IDs
SM-->>MC: Album status
alt Album not complete
MC->>FL: Get unposted photos
FL->>FA: Fetch album photos
FA-->>FL: Return photo metadata
FL-->>MC: Processed photo list
MC->>SM: Get next photo to post
SM-->>MC: Next unposted photo
MC->>CG: Generate caption
CG->>OA: Analyze image with GPT-4 Vision
OA-->>CG: Generated caption text
CG-->>MC: Complete Instagram caption
MC->>IG: Validate image URL
IG-->>MC: Validation result
MC->>IG: Post to Instagram
IG->>IA: Create media container
IA-->>IG: Container ID
IG->>IA: Publish container
IA-->>IG: Post ID
IG-->>MC: Success/failure
MC->>SM: Record post result
SM->>GI: Create tracking issue
GI-->>SM: Issue created
MC->>GA: Report completion
else Album complete
MC->>GA: Skip - all photos posted
end
graph LR
subgraph "Configuration Layer"
ENV[🔐 Environment Variables<br/>- API Keys<br/>- Album Settings<br/>- Credentials]
CFG[⚙️ Config Manager<br/>- Validation<br/>- API Endpoints<br/>- Settings]
end
subgraph "Service Layer"
FLS[📷 Flickr Service<br/>- Photo Retrieval<br/>- Metadata Extraction<br/>- URL Generation]
IGS[📱 Instagram Service<br/>- Media Upload<br/>- Publishing<br/>- Validation]
CPS[🤖 Caption Service<br/>- AI Generation<br/>- Text Processing<br/>- Retry Logic]
STS[📊 State Service<br/>- Progress Tracking<br/>- Issue Management<br/>- Completion Detection]
end
subgraph "Orchestration Layer"
MAIN[🎮 Main Controller<br/>- Workflow Coordination<br/>- Error Handling<br/>- Logging]
end
ENV --> CFG
CFG --> FLS
CFG --> IGS
CFG --> CPS
CFG --> STS
FLS --> MAIN
IGS --> MAIN
CPS --> MAIN
STS --> MAIN
classDef config fill:#fff3e0,stroke:#e65100,stroke-width:2px
classDef service fill:#e8f5e8,stroke:#1b5e20,stroke-width:2px
classDef orchestration fill:#e3f2fd,stroke:#0d47a1,stroke-width:2px
class ENV,CFG config
class FLS,IGS,CPS,STS service
class MAIN orchestration
graph TB
subgraph "Development"
DEV[👨💻 Local Development<br/>- .env files<br/>- Dry run testing<br/>- Debug logging]
end
subgraph "Repository"
REPO[📁 GitHub Repository<br/>- Source code<br/>- Configuration<br/>- Documentation]
end
subgraph "Production"
PROD[🏭 GitHub Actions<br/>- Automated execution<br/>- Secure credentials<br/>- Error handling]
end
subgraph "Monitoring"
LOGS[📊 Monitoring<br/>- GitHub Issues<br/>- Workflow logs<br/>- Artifacts]
end
DEV --> REPO
REPO --> PROD
PROD --> LOGS
LOGS --> DEV
classDef dev fill:#e8f5e8,stroke:#1b5e20,stroke-width:2px
classDef repo fill:#f3e5f5,stroke:#4a148c,stroke-width:2px
classDef prod fill:#e1f5fe,stroke:#01579b,stroke-width:2px
classDef monitor fill:#fff3e0,stroke:#e65100,stroke-width:2px
class DEV dev
class REPO repo
class PROD prod
class LOGS monitor
| Category | Component | Technology | Purpose |
|---|---|---|---|
| Infrastructure | Orchestration | GitHub Actions | Workflow automation, scheduling |
| Runtime | Python 3.11 | Core application logic | |
| State Storage | Git-Based Files | Secure, scalable state management with fine-grained permissions | |
| Configuration | Credentials | GitHub Secrets | Encrypted credential storage |
| Settings | Environment Variables | Secure configuration management | |
| External APIs | Photo Source | Flickr API | Photo metadata and URLs |
| Social Media | Instagram Graph API | Photo posting and publishing | |
| AI Services | OpenAI GPT-4 Vision | AI-powered caption generation | |
| Reliability | Error Handling | Exponential Backoff | Retry logic for API failures |
| Monitoring | Python Logging | Comprehensive audit trails | |
| Validation | URL Checking | Image accessibility verification |
- Visit Flickr App Garden
- Create a new app and get your API key
- Find your User ID from your Flickr profile URL
- Create a Facebook App at developers.facebook.com
- Add Instagram Graph API product
- Get a long-lived access token
- Connect your Instagram Business account
- Sign up at platform.openai.com
- Create an API key
- Ensure you have credits for GPT-4 Vision
- Runs automatically every day at 9 AM UTC
- Posts one photo per day from your album
- Automatically stops when all photos are posted
- No manual intervention required
Use the manual workflow trigger with these options:
- Dry Run: Test without posting to Instagram
- Show Stats: Display posting statistics and progress
# Install dependencies
pip install -r requirements.txt
# Post next photo (dry run)
python main.py --dry-run
# Post next photo (live)
python main.py
# Show statistics
python main.py --statsThe system uses Git-based file storage for modern, secure state management:
state/{account}/{album_id}/posts.json- Posted photo records with full audit trailstate/{account}/{album_id}/failed.json- Failed posting attempts for retry logicstate/{account}/{album_id}/metadata.json- Album statistics and completion tracking
- Fine-grained permissions: Only requires
contents:writePAT scope - Audit trail: Full Git history of all state changes
- Scalability: No size limits on state data
- Reliability: Atomic Git operations with conflict resolution
Run the workflow with "Show statistics only" checked, or use:
python main.py --statsThis shows:
- Total photos in album
- Photos posted so far
- Photos remaining
- Completion percentage
- Success rate
View Git-based state files to see:
- Each photo that was posted (in
state/{account}/{album_id}/posts.json) - Instagram post IDs and timestamps with full metadata
- Failed posting attempts and retry information
- Complete audit trail with Git history
When all photos in your album have been posted:
- 🎉 The automation displays "Album complete!"
- ⏸️ Scheduled runs automatically skip execution
- 📊 Statistics show 100% completion
- 🔄 To start a new album, update the
FLICKR_ALBUM_IDvariable in GitHub repository settings
├── main.py # Main automation script with multi-account support
├── config.py # Configuration management (environment variables)
├── account_config.py # Multi-account configuration with language support
├── blog_content_extractor.py # Enhanced blog content integration with WordPress auth
├── caption_generator.py # Multi-language caption generation with cultural awareness
├── flickr_api.py # Flickr API integration
├── instagram_api.py # Instagram posting with retry logic
├── state_manager.py # Account-isolated state management
├── storage_adapter.py # Git-based storage adapter with enhanced models
├── state_models.py # Enhanced data models for state management
├── test_suite/ # Comprehensive testing framework
│ ├── test_account_config.py # Multi-language account configuration tests
│ ├── test_blog_content.py # Blog content extraction and WordPress auth tests
│ ├── test_caption_generator.py # Multi-language caption generation tests
│ ├── test_exif_prioritization.py # EXIF URL prioritization tests
│ └── test_integration.py # End-to-end multi-account integration tests
├── requirements.txt # Python dependencies
└── .github/
└── workflows/
├── social-automation.yml # Reusable workflow (centralized logic)
├── primary-flickr-to-insta.yml # Primary account workflow
└── secondary-flickr-to-insta.yml # Secondary account workflow
"Bot traffic blocked" when accessing blog content
Failed to extract blog content: 403 Forbidden
- Cause: Website blocking automated requests without authentication
- Solution: Configure WordPress authentication credentials (
WORDPRESS_USERNAMEandWORDPRESS_APP_PASSWORD) for full content access WordPress authentication setup
- Go to your WordPress admin → Users → Profile
- Scroll down to "Application Passwords"
- Add a new application password with a descriptive name
- Copy the generated password (not your regular password)
- Add both
WORDPRESS_USERNAMEandWORDPRESS_APP_PASSWORDto GitHub repository secrets Limited blog content in captions
Using excerpt instead of full editorial content
- Cause: Unauthenticated requests only receive limited content excerpts
- Solution: WordPress authentication provides access to complete editorial content for richer caption generation
Enable debug logging for troubleshooting:
python main.py --log-level DEBUG --dry-runTest your Flickr album access:
# Check if your album is accessible
curl "https://www.flickr.com/services/rest/?method=flickr.photosets.getPhotos&api_key=YOUR_API_KEY&photoset_id=YOUR_ALBUM_ID&format=json&nojsoncallback=1"- Encrypted Secrets: All API credentials stored as GitHub Secrets
- Fine-grained PAT: Uses
contents:writepermission only (not broadreposcope) - Environment Protection: Environment-specific deployment protection rules
- Input Validation: Comprehensive validation for all external data
- Git-based State: Secure state management with full audit trails
- Zero Hardcoding: No credentials or sensitive data stored in code
- Minimal Permissions: Follows principle of least privilege throughout
For issues and questions:
- Check Configuration: Verify your GitHub repository variables and secrets
- Test with Dry Run: Use
--dry-runflag to test without posting - Review Logs: Check GitHub Actions logs and artifacts
- Statistics: Use
--statsto check progress and identify issues - GitHub Issues: Don't create an issue because I don't have the resources to follow up Use this repo for free. It comes as is, i.e. without any warranty whatsoever. I don't offer support due to limited time.
Here's what a typical automation cycle looks like:
- Day 1: Posts photo 1/13 from your album
- Day 2: Posts photo 2/13 from your album
- Day 3: Posts photo 3/13 from your album
- ...
- Day 13: Posts photo 13/13 from your album
- Day 14: Shows "Album complete!" and stops The automation intelligently tracks which photos have been posted and always selects the next unposted photo in the album order.
I'm working on this repo in a very limited capacity. Therefore, I can't review PRs or develop the functionality any further.
This project is licensed under the MIT License.