Verifying admin access...
System Architecture
Complete technical reference for admins. Covers the full stack: ad generation pipeline, A2A protocol, AP2 payments, authentication, SCTE-35 ad insertion, CLI, deployment, and DJ integration.
High-level architecture and how each layer connects
Galactic Radio Ads is a production FastAPI service that generates broadcast-quality AI radio advertisements with natural-sounding TTS audio. It supports PDF-to-ad conversion, legal disclaimer overlays, SCTE-35 metadata for broadcast insertion, and serves three types of clients simultaneously:
How a radio ad goes from request to broadcast-ready audio in 7 steps
User submits ad name, business name, ad copy, tone, voice, duration, and optional legal text. Business credit is deducted (1 credit per ad generation). Alternatively, a PDF can be uploaded.
POST /api/v1/ads/create
If a PDF file is uploaded, Gemini Vision extracts business details, ad copy, and key selling points from the document. Extracted data supplements or replaces manual inputs.
POST /api/v1/ads/create-from-pdf
Sends ad copy + business context + system prompt to Gemini 2.5 Pro. The LLM writes a broadcast-ready ad script. If a DJ personality is selected, the DJ's traits, catchphrases, and style are injected into the prompt.
LLMService.generate_ad_script()
Converts the main ad script to natural-sounding audio. Supports Gemini TTS (default) and ElevenLabs as providers. Each voice has unique characteristics suited for ad reads.
TTSProvider.generate_audio(script, voice_id)
If legal text is provided, it is converted to speech separately using a fast, clear voice at a quicker pace. This ensures the legal disclaimer is audible but compact.
TTSProvider.generate_audio(legal_text, legal_voice)
Uses FFmpeg to concatenate the main ad audio and legal disclaimer, compress to target duration (15s or 30s), and optimize quality. Format conversion and loudness normalization are applied.
AudioProcessingService.concat_and_compress()
Final MP3 is uploaded to Google Cloud Storage with a signed URL. SCTE-35 ad insertion metadata is generated and stored alongside the ad record for broadcast integration.
StorageService.upload_audio() → signed_url + SCTE-35
Three auth methods serving different client types
Google Sign-In popup via Firebase Auth SDK. The frontend sends the Firebase ID token as a Bearer token. Backend verifies with Firebase Admin SDK.
Uses a shared Firebase project (grn-weather-auth-only) dedicated to authentication across the Galactic Radio Network, separate from the main GCP project.
User API keys (prefixed gra_) generated at signup or in the Account tab. Sent via X-API-Key header. Validated against Firestore.
For service-to-service communication. Configured in SERVICE_API_KEYS env var. Grants SERVICE role with full access.
require_admin() dependency. Local dev bypasses auth entirely with ADMIN role.
Business-scoped credits with tiered subscription plans
1 credit = 1 ad generation. Checking ad status (get_ad_status skill) is free. Credits are deducted before generation begins and refunded on failure.
Business-scoped credits: Unlike the Weather app where credits are user-scoped, Ads credits are tied to a business. All users associated with a business share a single credit pool. The business owner manages the subscription, and all members draw from the business balance.
Users set a default business name in their profile. Credits are deducted from the business associated with the ad, not from the individual user.
Google's open protocol for AI agents to discover and use our ad service
The A2A layer runs alongside the existing REST API — it doesn't replace anything. Any A2A-compatible agent can discover our service via the Agent Card, then interact using JSON-RPC messages.
| Method | Description |
|---|---|
message/send | Send a message to the agent, returns task result |
message/stream | Send + stream progress via SSE events |
tasks/get | Get current status and artifacts of a task |
tasks/cancel | Cancel a running generation task |
How AI agents pay for ad credit purchases
AP2 is Google's protocol for agent-to-merchant payments using cryptographic IntentMandates. Agents include a payment mandate in the A2A message DataPart. Our service validates the mandate, adds credits to the business account, and returns a PaymentReceipt.
Included as a DataPart with key ap2.mandates.IntentMandate in the A2A message. Contains expiry, merchant restrictions, and natural language description of the credit purchase.
Check expiry timestamp (must be future), verify merchant name matches "Galactic Radio Ads" if merchants list is specified.
AP2PaymentHandler.validate_mandate()
If valid, authorize the payment and add credits to the specified business account. The purchase_credits skill handles the credit allocation.
On success, a ap2.PaymentReceipt DataPart is included in the response artifacts with amount, currency, credits added, and merchant confirmation ID.
DJ Builder API integration for personality-driven ads
DJ personalities are fetched from the DJ Builder API and injected into the LLM prompt during script generation. This allows ads to be read in the style of specific radio personalities, complete with their catchphrases, vocal quirks, and presentation style.
User picks from available DJ personalities via the /api/v1/dj/list endpoint. Each DJ has a unique name, traits, catchphrases, and delivery style.
The DJ's personality profile (traits, catchphrases, vocal style) is injected into the system prompt alongside the ad copy. The LLM writes the script as if the DJ is delivering it live on air.
The TTS voice is matched to the DJ's preferred voice setting, or the user can override with a specific voice selection.
Broadcast metadata for automated ad insertion
SCTE-35 is the industry-standard protocol for signaling ad insertion points in broadcast streams. When an ad is generated, Galactic Radio Ads produces SCTE-35-compatible metadata that broadcast systems can use to automatically insert the ad at the correct points in the stream.
{
"scte35": {
"splice_command_type": "splice_insert",
"splice_event_id": "ad_<ad_id>",
"duration_ms": 30000,
"avail_num": 1,
"avails_expected": 1,
"break_duration": {
"auto_return": true,
"duration": 30000
}
},
"ad_metadata": {
"ad_id": "<uuid>",
"business_name": "Acme Corp",
"ad_name": "Summer Sale Promo",
"audio_url": "https://storage.googleapis.com/...",
"duration_seconds": 30,
"network_active": true,
"start_date": "2026-04-15",
"end_date": "2026-05-15"
}
}
When ads are marked as "Active on Galactic Radio Network" with start/end dates, they become available for automated scheduling across the network. Broadcast systems query the /api/v1/network/ads endpoint to pull currently active ads with their SCTE-35 metadata.
start_date and end_date. The network endpoint filters by these dates automatically.
REST endpoints and their purpose
| Method | Endpoint | Description | Auth |
|---|---|---|---|
| Health & System | |||
| GET | /api/v1/health | Health check | None |
| Ads (CRUD & Generation) | |||
| POST | /api/v1/ads/create | Create ad from text input | Required |
| POST | /api/v1/ads/create-from-pdf | Create ad from uploaded PDF | Required |
| GET | /api/v1/ads/status/{job_id} | Poll ad generation job status | Required |
| GET | /api/v1/ads/{ad_id} | Get ad details | Required |
| GET | /api/v1/ads | List user's ads | Required |
| DELETE | /api/v1/ads/{ad_id} | Delete an ad | Required |
| Users & Auth | |||
| POST | /api/v1/users/oauth-signin | Create/get user profile | Firebase |
| GET | /api/v1/users/me | Get current user profile | Required |
| PATCH | /api/v1/users/me | Update user profile (business name) | Required |
| POST | /api/v1/users/api-keys | Generate new API key (gra_xxx) | Required |
| Voices & Files | |||
| GET | /api/v1/voices | List available TTS voices | Required |
| GET | /api/v1/files | List user's audio files | Required |
| Subscriptions & Businesses | |||
| POST | /api/v1/subscriptions/checkout | Change subscription plan | Required |
| GET | /api/v1/businesses | List user's businesses | Required |
| POST | /api/v1/businesses | Create a business | Required |
| Network & DJ | |||
| GET | /api/v1/network/ads | Get active network ads with SCTE-35 | Service |
| POST | /api/v1/dj/generate | Generate DJ-style ad | Required |
| GET | /api/v1/dj/list | List DJ personalities | Required |
| A2A Protocol Endpoints | |||
| GET | /.well-known/agent.json | Agent Card discovery | None |
| POST | /a2a | A2A JSON-RPC endpoint | Required |
gra)Command-line interface for power users and automation
Install: pip install -e ".[cli]" — then use gra from the terminal.
gra auth login
Authenticate with API key
gra auth status
Check current auth status and credits
gra create "Summer Sale" --business "Acme Corp" --copy "50% off everything!"
Create a radio ad with text input
gra create "Promo" --pdf ./flyer.pdf --duration 15 --tone energetic
Create an ad from a PDF file
gra create "Grand Opening" --business "Joe's Pizza" --dj <uuid>
Create a DJ-style ad with a personality overlay
gra ads
List all your ads
gra ads status <job_id>
Check ad generation job status
gra voices
List all available TTS voices
gra files
List your audio files
gra plans
View subscription plans and pricing
gra admin users
List all users (admin only)
gra admin stats
View system statistics (admin only)
gra agent card
Fetch and display the Agent Card
gra agent send "Create a 30s ad for Acme Corp" --stream
Send an A2A message with SSE streaming
gra agent task <task_id>
Get A2A task status
gra agent cancel <task_id>
Cancel a running A2A task
Cloud Build, Docker, and Cloud Run configuration
ENVIRONMENT=production GCP_PROJECT_ID=gen-lang-client-0957930691 GCS_BUCKET_NAME=galactic-radio-ads-audio GEMINI_API_KEY=*** FIREBASE_PROJECT_ID=gen-lang-client-0957930691 FIREBASE_AUTH_PROJECT_ID=grn-weather-auth-only A2A_ENABLED=true AP2_ENABLED=true A2A_AGENT_URL=https://galactic-radio-ads-***.run.app SERVICE_API_KEYS=*** RATE_LIMIT_PER_MINUTE=60 RATE_LIMIT_GENERATE_PER_MINUTE=10
--set-env-vars flag replaces ALL env vars on deploy. All env vars must be in cloudbuild.yaml or they'll be wiped.
Every tool and service in the system
galactic-radio-ads/
src/galactic_ads/
app.py # FastAPI application factory
config.py # Pydantic settings from env vars
dependencies.py # ServiceContainer (all singletons)
middleware.py # Rate limiting, CORS
exceptions.py # Error handlers
auth/
firebase.py # Firebase token verification
dependencies.py # get_current_user, require_admin
api/v1/
router.py # REST endpoint registration
ads.py # /ads/create, /ads/status, CRUD
users.py # /users/me, /users/api-keys
dj.py # /dj/generate, /dj/list
voices.py # /voices list
files.py # /files list, download, delete
subscriptions.py # /subscriptions/checkout
businesses.py # /businesses CRUD
network.py # /network/ads (SCTE-35 feed)
a2a/
setup.py # Mount A2A routes on FastAPI
agent_card.py # Build AgentCard with skills
executor.py # GalacticAdsExecutor
task_store.py # FirestoreTaskStore (A2A <-> Firestore)
skill_router.py # Route messages to skills
message_parser.py # Extract params from A2A messages
auth_bridge.py # Bridge A2A auth to existing system
ap2_handler.py # AP2 mandate validation + receipts
services/
generation.py # GenerationOrchestrator (LLM+TTS+Audio)
llm.py # Gemini LLM wrapper
tts/ # TTS providers (Gemini, ElevenLabs)
audio.py # FFmpeg audio processing (concat+compress)
pdf_extract.py # Gemini Vision PDF-to-text
storage.py # GCS upload/download
credit.py # Business-scoped credit check/deduct/refund
dj.py # DJ personality service
models/
domain.py # Pydantic models (AuthenticatedUser, Ad, etc.)
enums.py # Status enums (JobStatus, UserRole)
repositories/
ad_repo.py # Firestore ad CRUD
job_repo.py # Firestore job CRUD
user_repo.py # Firestore user CRUD
business_repo.py # Firestore business CRUD
frontend/
router.py # Serve static HTML pages
static/ # HTML, CSS, JS, favicon
cli/
main.py # Typer CLI entrypoint (gra)
commands/
auth.py # gra auth
create.py # gra create
ads.py # gra ads
agent.py # gra agent (A2A commands)
admin.py # gra admin
display.py # Rich terminal output helpers
tests/
unit/ # Unit tests
integration/ # Integration tests
Dockerfile # python:3.12-slim + ffmpeg + git
cloudbuild.yaml # Cloud Build -> Cloud Run deploy
pyproject.toml # Project metadata + deps