Compare commits
1 Commits
codex/wrap
...
codex/veri
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6555a722d3 |
43
artifacts/podcast_billing_sequence_report.json
Normal file
43
artifacts/podcast_billing_sequence_report.json
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
{
|
||||||
|
"preflight": {
|
||||||
|
"success": true,
|
||||||
|
"can_proceed": true,
|
||||||
|
"estimated_cost": 0.3
|
||||||
|
},
|
||||||
|
"operations": {
|
||||||
|
"analysis_title_suggestions": [
|
||||||
|
"AI Agents in 2026",
|
||||||
|
"Ship Faster with AI",
|
||||||
|
"Startup AI Playbook"
|
||||||
|
],
|
||||||
|
"research_provider": "exa",
|
||||||
|
"research_cost": 0.015,
|
||||||
|
"video_task_status": "completed"
|
||||||
|
},
|
||||||
|
"dashboard_deltas": {
|
||||||
|
"total_calls_before": 1,
|
||||||
|
"total_calls_after": 5,
|
||||||
|
"delta_calls": 4,
|
||||||
|
"total_cost_before": 0.09,
|
||||||
|
"total_cost_after": 0.488,
|
||||||
|
"delta_cost": 0.398,
|
||||||
|
"projected_monthly_cost_before": 0.09,
|
||||||
|
"projected_monthly_cost_after": 0.49,
|
||||||
|
"delta_projected_monthly_cost": 0.4
|
||||||
|
},
|
||||||
|
"provider_cost_deltas": {
|
||||||
|
"exa": 0.005,
|
||||||
|
"huggingface": 0.003,
|
||||||
|
"wavespeed": 0.39
|
||||||
|
},
|
||||||
|
"acceptance": {
|
||||||
|
"passed": true,
|
||||||
|
"criteria": {
|
||||||
|
"preflight_success": true,
|
||||||
|
"usage_cost_incremented": true,
|
||||||
|
"usage_call_incremented": true,
|
||||||
|
"projection_incremented": true,
|
||||||
|
"provider_delta_present": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
351
backend/app.py
351
backend/app.py
@@ -48,8 +48,6 @@ load_dotenv(backend_dir / '.env') # backend/.env
|
|||||||
load_dotenv(project_root / '.env') # root .env (fallback)
|
load_dotenv(project_root / '.env') # root .env (fallback)
|
||||||
load_dotenv() # CWD .env (fallback)
|
load_dotenv() # CWD .env (fallback)
|
||||||
|
|
||||||
PODCAST_ONLY_DEMO_MODE = os.getenv("PODCAST_ONLY_DEMO_MODE", "false").lower() in {"1", "true", "yes", "on"}
|
|
||||||
|
|
||||||
# Set up clean logging for end users
|
# Set up clean logging for end users
|
||||||
from logging_config import setup_clean_logging
|
from logging_config import setup_clean_logging
|
||||||
setup_clean_logging()
|
setup_clean_logging()
|
||||||
@@ -112,37 +110,36 @@ from services.startup_health import (
|
|||||||
# Import OAuth token monitoring routes
|
# Import OAuth token monitoring routes
|
||||||
from api.oauth_token_monitoring_routes import router as oauth_token_monitoring_router
|
from api.oauth_token_monitoring_routes import router as oauth_token_monitoring_router
|
||||||
|
|
||||||
if not PODCAST_ONLY_DEMO_MODE:
|
# Import SEO Dashboard endpoints
|
||||||
# Import SEO Dashboard endpoints only when non-demo features are enabled
|
from api.seo_dashboard import (
|
||||||
from api.seo_dashboard import (
|
get_seo_dashboard_data,
|
||||||
get_seo_dashboard_data,
|
get_seo_health_score,
|
||||||
get_seo_health_score,
|
get_seo_metrics,
|
||||||
get_seo_metrics,
|
get_platform_status,
|
||||||
get_platform_status,
|
get_ai_insights,
|
||||||
get_ai_insights,
|
seo_dashboard_health_check,
|
||||||
seo_dashboard_health_check,
|
analyze_seo_comprehensive,
|
||||||
analyze_seo_comprehensive,
|
analyze_seo_full,
|
||||||
analyze_seo_full,
|
get_seo_metrics_detailed,
|
||||||
get_seo_metrics_detailed,
|
get_analysis_summary,
|
||||||
get_analysis_summary,
|
batch_analyze_urls,
|
||||||
batch_analyze_urls,
|
SEOAnalysisRequest,
|
||||||
SEOAnalysisRequest,
|
get_seo_dashboard_overview,
|
||||||
get_seo_dashboard_overview,
|
get_gsc_raw_data,
|
||||||
get_gsc_raw_data,
|
get_bing_raw_data,
|
||||||
get_bing_raw_data,
|
get_competitive_insights,
|
||||||
get_competitive_insights,
|
get_deep_competitor_analysis,
|
||||||
get_deep_competitor_analysis,
|
run_strategic_insights,
|
||||||
run_strategic_insights,
|
get_strategic_insights_history,
|
||||||
get_strategic_insights_history,
|
refresh_analytics_data,
|
||||||
refresh_analytics_data,
|
analyze_urls_ai,
|
||||||
analyze_urls_ai,
|
AnalyzeURLsRequest,
|
||||||
AnalyzeURLsRequest,
|
get_analyzed_pages,
|
||||||
get_analyzed_pages,
|
get_semantic_health,
|
||||||
get_semantic_health,
|
get_semantic_cache_stats,
|
||||||
get_semantic_cache_stats,
|
get_sif_indexing_health,
|
||||||
get_sif_indexing_health,
|
get_onboarding_task_health,
|
||||||
get_onboarding_task_health,
|
)
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# Initialize FastAPI app
|
# Initialize FastAPI app
|
||||||
@@ -262,184 +259,194 @@ async def onboarding_status():
|
|||||||
return onboarding_manager.get_onboarding_status()
|
return onboarding_manager.get_onboarding_status()
|
||||||
|
|
||||||
# Include routers using modular utilities
|
# Include routers using modular utilities
|
||||||
if not PODCAST_ONLY_DEMO_MODE:
|
router_manager.include_core_routers()
|
||||||
router_manager.include_core_routers()
|
router_manager.include_optional_routers()
|
||||||
router_manager.include_optional_routers()
|
|
||||||
else:
|
|
||||||
logger.info("PODCAST_ONLY_DEMO_MODE enabled: including only podcast and subscription feature routers.")
|
|
||||||
app.include_router(subscription_router)
|
|
||||||
|
|
||||||
# Include assets serving router (must be mounted to serve generated images)
|
# Include assets serving router (must be mounted to serve generated images)
|
||||||
app.include_router(assets_serving_router)
|
app.include_router(assets_serving_router)
|
||||||
|
|
||||||
if not PODCAST_ONLY_DEMO_MODE:
|
# SEO Dashboard endpoints
|
||||||
# SEO Dashboard endpoints
|
@app.get("/api/seo-dashboard/data")
|
||||||
@app.get("/api/seo-dashboard/data")
|
async def seo_dashboard_data():
|
||||||
async def seo_dashboard_data():
|
"""Get complete SEO dashboard data."""
|
||||||
"""Get complete SEO dashboard data."""
|
return await get_seo_dashboard_data()
|
||||||
return await get_seo_dashboard_data()
|
|
||||||
|
|
||||||
@app.get("/api/seo-dashboard/health-score")
|
@app.get("/api/seo-dashboard/health-score")
|
||||||
async def seo_health_score():
|
async def seo_health_score():
|
||||||
"""Get SEO health score."""
|
"""Get SEO health score."""
|
||||||
return await get_seo_health_score()
|
return await get_seo_health_score()
|
||||||
|
|
||||||
@app.get("/api/seo-dashboard/metrics")
|
@app.get("/api/seo-dashboard/metrics")
|
||||||
async def seo_metrics():
|
async def seo_metrics():
|
||||||
"""Get SEO metrics."""
|
"""Get SEO metrics."""
|
||||||
return await get_seo_metrics()
|
return await get_seo_metrics()
|
||||||
|
|
||||||
@app.get("/api/seo-dashboard/platforms")
|
@app.get("/api/seo-dashboard/platforms")
|
||||||
async def seo_platforms(current_user: dict = Depends(get_current_user)):
|
async def seo_platforms(current_user: dict = Depends(get_current_user)):
|
||||||
"""Get platform status."""
|
"""Get platform status."""
|
||||||
return await get_platform_status(current_user)
|
return await get_platform_status(current_user)
|
||||||
|
|
||||||
@app.get("/api/seo-dashboard/insights")
|
@app.get("/api/seo-dashboard/insights")
|
||||||
async def seo_insights():
|
async def seo_insights():
|
||||||
"""Get AI insights."""
|
"""Get AI insights."""
|
||||||
return await get_ai_insights()
|
return await get_ai_insights()
|
||||||
|
|
||||||
# New SEO Dashboard endpoints with real data
|
# New SEO Dashboard endpoints with real data
|
||||||
@app.get("/api/seo-dashboard/overview")
|
@app.get("/api/seo-dashboard/overview")
|
||||||
async def seo_dashboard_overview_endpoint(current_user: dict = Depends(get_current_user), site_url: str = None):
|
async def seo_dashboard_overview_endpoint(current_user: dict = Depends(get_current_user), site_url: str = None):
|
||||||
"""Get comprehensive SEO dashboard overview with real GSC/Bing data."""
|
"""Get comprehensive SEO dashboard overview with real GSC/Bing data."""
|
||||||
return await get_seo_dashboard_overview(current_user, site_url)
|
return await get_seo_dashboard_overview(current_user, site_url)
|
||||||
|
|
||||||
@app.get("/api/seo-dashboard/gsc/raw")
|
@app.get("/api/seo-dashboard/gsc/raw")
|
||||||
async def gsc_raw_data_endpoint(current_user: dict = Depends(get_current_user), site_url: str = None):
|
async def gsc_raw_data_endpoint(current_user: dict = Depends(get_current_user), site_url: str = None):
|
||||||
"""Get raw GSC data for the specified site."""
|
"""Get raw GSC data for the specified site."""
|
||||||
return await get_gsc_raw_data(current_user, site_url)
|
return await get_gsc_raw_data(current_user, site_url)
|
||||||
|
|
||||||
@app.get("/api/seo-dashboard/bing/raw")
|
@app.get("/api/seo-dashboard/bing/raw")
|
||||||
async def bing_raw_data_endpoint(current_user: dict = Depends(get_current_user), site_url: str = None):
|
async def bing_raw_data_endpoint(current_user: dict = Depends(get_current_user), site_url: str = None):
|
||||||
"""Get raw Bing data for the specified site."""
|
"""Get raw Bing data for the specified site."""
|
||||||
return await get_bing_raw_data(current_user, site_url)
|
return await get_bing_raw_data(current_user, site_url)
|
||||||
|
|
||||||
@app.get("/api/seo-dashboard/competitive-insights")
|
@app.get("/api/seo-dashboard/competitive-insights")
|
||||||
async def competitive_insights_endpoint(current_user: dict = Depends(get_current_user), site_url: str = None):
|
async def competitive_insights_endpoint(current_user: dict = Depends(get_current_user), site_url: str = None):
|
||||||
"""Get competitive insights from onboarding step 3 data."""
|
"""Get competitive insights from onboarding step 3 data."""
|
||||||
return await get_competitive_insights(current_user, site_url)
|
return await get_competitive_insights(current_user, site_url)
|
||||||
|
|
||||||
@app.get("/api/seo-dashboard/deep-competitor-analysis")
|
@app.get("/api/seo-dashboard/deep-competitor-analysis")
|
||||||
async def deep_competitor_analysis_endpoint(current_user: dict = Depends(get_current_user), site_url: str = None):
|
async def deep_competitor_analysis_endpoint(current_user: dict = Depends(get_current_user), site_url: str = None):
|
||||||
"""Get deep competitor analysis results (auto-scheduled post-onboarding)."""
|
"""Get deep competitor analysis results (auto-scheduled post-onboarding)."""
|
||||||
return await get_deep_competitor_analysis(current_user, site_url)
|
return await get_deep_competitor_analysis(current_user, site_url)
|
||||||
|
|
||||||
@app.post("/api/seo-dashboard/strategic-insights/run")
|
@app.post("/api/seo-dashboard/strategic-insights/run")
|
||||||
async def run_strategic_insights_endpoint(current_user: dict = Depends(get_current_user)):
|
async def run_strategic_insights_endpoint(current_user: dict = Depends(get_current_user)):
|
||||||
"""Run AI-powered strategic insights analysis manually."""
|
"""Run AI-powered strategic insights analysis manually."""
|
||||||
return await run_strategic_insights(current_user)
|
return await run_strategic_insights(current_user)
|
||||||
|
|
||||||
@app.get("/api/seo-dashboard/strategic-insights/history")
|
@app.get("/api/seo-dashboard/strategic-insights/history")
|
||||||
async def get_strategic_insights_history_endpoint(current_user: dict = Depends(get_current_user)):
|
async def get_strategic_insights_history_endpoint(current_user: dict = Depends(get_current_user)):
|
||||||
"""Fetch the history of strategic insights for the user."""
|
"""Fetch the history of strategic insights for the user."""
|
||||||
return await get_strategic_insights_history(current_user)
|
return await get_strategic_insights_history(current_user)
|
||||||
|
|
||||||
@app.post("/api/seo-dashboard/refresh")
|
@app.post("/api/seo-dashboard/refresh")
|
||||||
async def refresh_analytics_data_endpoint(current_user: dict = Depends(get_current_user), site_url: str = None):
|
async def refresh_analytics_data_endpoint(current_user: dict = Depends(get_current_user), site_url: str = None):
|
||||||
"""Refresh analytics data by invalidating cache and fetching fresh data."""
|
"""Refresh analytics data by invalidating cache and fetching fresh data."""
|
||||||
return await refresh_analytics_data(current_user, site_url)
|
return await refresh_analytics_data(current_user, site_url)
|
||||||
|
|
||||||
@app.get("/api/seo-dashboard/onboarding-task-health")
|
|
||||||
async def onboarding_task_health_endpoint(current_user: dict = Depends(get_current_user), site_url: str = None):
|
|
||||||
"""Get consolidated health for onboarding-scheduled SEO tasks."""
|
|
||||||
return await get_onboarding_task_health(current_user, site_url)
|
|
||||||
|
|
||||||
@app.get("/api/seo-dashboard/health")
|
|
||||||
async def seo_dashboard_health():
|
|
||||||
"""Health check for SEO dashboard."""
|
|
||||||
return await seo_dashboard_health_check()
|
|
||||||
|
|
||||||
# Phase 2B: Semantic health monitoring endpoint (24-hour polling)
|
@app.get("/api/seo-dashboard/onboarding-task-health")
|
||||||
@app.get("/api/seo-dashboard/semantic-health")
|
async def onboarding_task_health_endpoint(current_user: dict = Depends(get_current_user), site_url: str = None):
|
||||||
async def semantic_health_endpoint(current_user: dict = Depends(get_current_user)):
|
"""Get consolidated health for onboarding-scheduled SEO tasks."""
|
||||||
"""Get real-time semantic health metrics for content and competitors."""
|
return await get_onboarding_task_health(current_user, site_url)
|
||||||
return await get_semantic_health(current_user)
|
|
||||||
|
|
||||||
@app.get("/api/seo-dashboard/cache-stats")
|
@app.get("/api/seo-dashboard/health")
|
||||||
async def semantic_cache_stats_endpoint(current_user: dict = Depends(get_current_user)):
|
async def seo_dashboard_health():
|
||||||
"""Get semantic cache performance statistics."""
|
"""Health check for SEO dashboard."""
|
||||||
return await get_semantic_cache_stats(current_user)
|
return await seo_dashboard_health_check()
|
||||||
|
|
||||||
@app.get("/api/seo-dashboard/sif-health")
|
# Phase 2B: Semantic health monitoring endpoint (24-hour polling)
|
||||||
async def sif_indexing_health_endpoint(current_user: dict = Depends(get_current_user)):
|
@app.get("/api/seo-dashboard/semantic-health")
|
||||||
"""Get SIF indexing health summary for the current user."""
|
async def semantic_health_endpoint(current_user: dict = Depends(get_current_user)):
|
||||||
return await get_sif_indexing_health(current_user)
|
"""
|
||||||
|
Get real-time semantic health metrics for content and competitors.
|
||||||
|
This endpoint provides Phase 2B semantic intelligence monitoring data.
|
||||||
|
|
||||||
|
Returns semantic health score, status, and recommendations.
|
||||||
|
Data is cached and updated every 24 hours via scheduler.
|
||||||
|
"""
|
||||||
|
return await get_semantic_health(current_user)
|
||||||
|
|
||||||
# Comprehensive SEO Analysis endpoints
|
|
||||||
@app.post("/api/seo-dashboard/analyze-comprehensive")
|
|
||||||
async def analyze_seo_comprehensive_endpoint(request: SEOAnalysisRequest):
|
|
||||||
"""Analyze a URL for comprehensive SEO performance."""
|
|
||||||
return await analyze_seo_comprehensive(request)
|
|
||||||
|
|
||||||
@app.post("/api/seo-dashboard/analyze-full")
|
@app.get("/api/seo-dashboard/cache-stats")
|
||||||
async def analyze_seo_full_endpoint(request: SEOAnalysisRequest):
|
async def semantic_cache_stats_endpoint(current_user: dict = Depends(get_current_user)):
|
||||||
"""Analyze a URL for comprehensive SEO performance."""
|
"""
|
||||||
return await analyze_seo_full(request)
|
Get semantic cache performance statistics.
|
||||||
|
Returns hit rate, memory usage, and eviction counts.
|
||||||
|
"""
|
||||||
|
return await get_semantic_cache_stats(current_user)
|
||||||
|
|
||||||
@app.get("/api/seo-dashboard/metrics-detailed")
|
|
||||||
async def seo_metrics_detailed(url: str):
|
|
||||||
"""Get detailed SEO metrics for a URL."""
|
|
||||||
return await get_seo_metrics_detailed(url)
|
|
||||||
|
|
||||||
@app.get("/api/seo-dashboard/analysis-summary")
|
@app.get("/api/seo-dashboard/sif-health")
|
||||||
async def seo_analysis_summary(url: str):
|
async def sif_indexing_health_endpoint(current_user: dict = Depends(get_current_user)):
|
||||||
"""Get a quick summary of SEO analysis for a URL."""
|
"""
|
||||||
return await get_analysis_summary(url)
|
Get SIF indexing health summary for the current user.
|
||||||
|
Used by the Semantic Indexing Status widget on the dashboard.
|
||||||
|
"""
|
||||||
|
return await get_sif_indexing_health(current_user)
|
||||||
|
|
||||||
@app.post("/api/seo-dashboard/batch-analyze")
|
# Comprehensive SEO Analysis endpoints
|
||||||
async def batch_analyze_urls_endpoint(urls: list[str]):
|
@app.post("/api/seo-dashboard/analyze-comprehensive")
|
||||||
"""Analyze multiple URLs in batch."""
|
async def analyze_seo_comprehensive_endpoint(request: SEOAnalysisRequest):
|
||||||
return await batch_analyze_urls(urls)
|
"""Analyze a URL for comprehensive SEO performance."""
|
||||||
|
return await analyze_seo_comprehensive(request)
|
||||||
|
|
||||||
@app.post("/api/seo-dashboard/analyze-urls-ai")
|
@app.post("/api/seo-dashboard/analyze-full")
|
||||||
async def analyze_urls_ai_endpoint(request: AnalyzeURLsRequest, current_user: dict = Depends(get_current_user)):
|
async def analyze_seo_full_endpoint(request: SEOAnalysisRequest):
|
||||||
"""Run AI-powered SEO analysis on selected URLs."""
|
"""Analyze a URL for comprehensive SEO performance."""
|
||||||
return await analyze_urls_ai(request, current_user)
|
return await analyze_seo_full(request)
|
||||||
|
|
||||||
# Include platform analytics router
|
@app.get("/api/seo-dashboard/metrics-detailed")
|
||||||
from routers.platform_analytics import router as platform_analytics_router
|
async def seo_metrics_detailed(url: str):
|
||||||
app.include_router(platform_analytics_router)
|
"""Get detailed SEO metrics for a URL."""
|
||||||
# Include Bing Analytics Storage router to expose storage-backed endpoints
|
return await get_seo_metrics_detailed(url)
|
||||||
from routers.bing_analytics_storage import router as bing_analytics_storage_router
|
|
||||||
app.include_router(bing_analytics_storage_router)
|
|
||||||
app.include_router(images_router)
|
|
||||||
app.include_router(image_studio_router)
|
|
||||||
app.include_router(product_marketing_router)
|
|
||||||
app.include_router(campaign_creator_router)
|
|
||||||
|
|
||||||
# Include content assets router
|
@app.get("/api/seo-dashboard/analysis-summary")
|
||||||
from api.content_assets.router import router as content_assets_router
|
async def seo_analysis_summary(url: str):
|
||||||
app.include_router(content_assets_router)
|
"""Get a quick summary of SEO analysis for a URL."""
|
||||||
|
return await get_analysis_summary(url)
|
||||||
|
|
||||||
|
@app.post("/api/seo-dashboard/batch-analyze")
|
||||||
|
async def batch_analyze_urls_endpoint(urls: list[str]):
|
||||||
|
"""Analyze multiple URLs in batch."""
|
||||||
|
return await batch_analyze_urls(urls)
|
||||||
|
|
||||||
|
@app.post("/api/seo-dashboard/analyze-urls-ai")
|
||||||
|
async def analyze_urls_ai_endpoint(request: AnalyzeURLsRequest, current_user: dict = Depends(get_current_user)):
|
||||||
|
"""Run AI-powered SEO analysis on selected URLs."""
|
||||||
|
return await analyze_urls_ai(request, current_user)
|
||||||
|
|
||||||
|
# Include platform analytics router
|
||||||
|
from routers.platform_analytics import router as platform_analytics_router
|
||||||
|
app.include_router(platform_analytics_router)
|
||||||
|
# Include Bing Analytics Storage router to expose storage-backed endpoints
|
||||||
|
from routers.bing_analytics_storage import router as bing_analytics_storage_router
|
||||||
|
app.include_router(bing_analytics_storage_router)
|
||||||
|
app.include_router(images_router)
|
||||||
|
app.include_router(image_studio_router)
|
||||||
|
app.include_router(product_marketing_router)
|
||||||
|
app.include_router(campaign_creator_router)
|
||||||
|
|
||||||
|
# Include content assets router
|
||||||
|
from api.content_assets.router import router as content_assets_router
|
||||||
|
app.include_router(content_assets_router)
|
||||||
|
|
||||||
# Include Podcast Maker router
|
# Include Podcast Maker router
|
||||||
from api.podcast.router import router as podcast_router
|
from api.podcast.router import router as podcast_router
|
||||||
app.include_router(podcast_router)
|
app.include_router(podcast_router)
|
||||||
|
|
||||||
if not PODCAST_ONLY_DEMO_MODE:
|
# Include YouTube Creator Studio router
|
||||||
# Include YouTube Creator Studio router
|
from api.youtube.router import router as youtube_router
|
||||||
from api.youtube.router import router as youtube_router
|
app.include_router(youtube_router, prefix="/api")
|
||||||
app.include_router(youtube_router, prefix="/api")
|
|
||||||
|
|
||||||
# Include research configuration router
|
# Include research configuration router
|
||||||
app.include_router(research_config_router, prefix="/api/research", tags=["research"])
|
app.include_router(research_config_router, prefix="/api/research", tags=["research"])
|
||||||
|
|
||||||
# Include Research Engine router (standalone AI research module)
|
# Include Research Engine router (standalone AI research module)
|
||||||
from api.research.router import router as research_engine_router
|
from api.research.router import router as research_engine_router
|
||||||
app.include_router(research_engine_router, tags=["Research Engine"])
|
app.include_router(research_engine_router, tags=["Research Engine"])
|
||||||
|
|
||||||
# Scheduler dashboard routes
|
# Scheduler dashboard routes
|
||||||
from api.scheduler_dashboard import router as scheduler_dashboard_router
|
from api.scheduler_dashboard import router as scheduler_dashboard_router
|
||||||
app.include_router(scheduler_dashboard_router)
|
app.include_router(scheduler_dashboard_router)
|
||||||
app.include_router(oauth_token_monitoring_router)
|
app.include_router(oauth_token_monitoring_router)
|
||||||
|
|
||||||
# Autonomous Agents API routes (Phase 3A)
|
# Autonomous Agents API routes (Phase 3A)
|
||||||
from api.agents_api import router as agents_router
|
from api.agents_api import router as agents_router
|
||||||
app.include_router(agents_router)
|
app.include_router(agents_router)
|
||||||
|
|
||||||
# Today workflow routes
|
# Today workflow routes
|
||||||
from api.today_workflow import router as today_workflow_router
|
from api.today_workflow import router as today_workflow_router
|
||||||
app.include_router(today_workflow_router)
|
app.include_router(today_workflow_router)
|
||||||
|
|
||||||
# Setup frontend serving using modular utilities
|
# Setup frontend serving using modular utilities
|
||||||
frontend_serving.setup_frontend_serving()
|
frontend_serving.setup_frontend_serving()
|
||||||
|
|||||||
355
backend/scripts/run_podcast_billing_sequence.py
Normal file
355
backend/scripts/run_podcast_billing_sequence.py
Normal file
@@ -0,0 +1,355 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Run podcast preflight + operations and verify billing usage/cost deltas."""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import json
|
||||||
|
import asyncio
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
# Use mock auth in local test runs
|
||||||
|
os.environ.setdefault("DISABLE_AUTH", "true")
|
||||||
|
os.environ.setdefault("ALLOW_UNVERIFIED_JWT_DEV", "true")
|
||||||
|
os.environ.setdefault(
|
||||||
|
"STRIPE_PLAN_PRICE_MAPPING_TEST",
|
||||||
|
"{\"basic\": {\"monthly\": \"price_test_basic_monthly\"}, \"pro\": {\"monthly\": \"price_test_pro_monthly\"}}",
|
||||||
|
)
|
||||||
|
os.environ.setdefault("EXA_API_KEY", "test-exa-key")
|
||||||
|
|
||||||
|
import spacy
|
||||||
|
|
||||||
|
# Avoid hard dependency on downloaded spaCy model during router imports.
|
||||||
|
spacy.load = lambda _name, *args, **kwargs: object() # type: ignore[assignment]
|
||||||
|
|
||||||
|
from fastapi import FastAPI
|
||||||
|
from fastapi.testclient import TestClient
|
||||||
|
|
||||||
|
# Import only required routers (avoids heavyweight app startup deps)
|
||||||
|
from api.podcast.router import router as podcast_router
|
||||||
|
from api.subscription import router as subscription_router
|
||||||
|
from api.podcast.handlers import analysis as analysis_handler
|
||||||
|
from api.podcast.handlers import research as research_handler
|
||||||
|
from api.podcast.handlers import video as video_handler
|
||||||
|
from api.podcast.constants import get_podcast_media_dir, PODCAST_IMAGES_DIR
|
||||||
|
from services.database import get_session_for_user
|
||||||
|
from services.subscription.usage_tracking_service import UsageTrackingService
|
||||||
|
from models.subscription_models import APIProvider
|
||||||
|
|
||||||
|
|
||||||
|
USER_ID = "mock_user_id"
|
||||||
|
AUTH_HEADERS = {"Authorization": "Bearer test-token"}
|
||||||
|
BILLING_PERIOD = "2026-03"
|
||||||
|
|
||||||
|
|
||||||
|
def _ensure_test_media_files(user_id: str) -> tuple[str, str]:
|
||||||
|
audio_dir = get_podcast_media_dir("audio", user_id, ensure_exists=True)
|
||||||
|
image_dir = get_podcast_media_dir("image", user_id, ensure_exists=True)
|
||||||
|
|
||||||
|
audio_file = audio_dir / "sequence_test_audio.mp3"
|
||||||
|
image_file = image_dir / "sequence_test_image.png"
|
||||||
|
|
||||||
|
if not audio_file.exists():
|
||||||
|
audio_file.write_bytes(b"ID3" + b"\x00" * 512)
|
||||||
|
if not image_file.exists():
|
||||||
|
# Minimal PNG header-like bytes (sufficient for mocked pipeline)
|
||||||
|
image_file.write_bytes(b"\x89PNG\r\n\x1a\n" + b"\x00" * 512)
|
||||||
|
# Also place in legacy global dir for URL resolver compatibility.
|
||||||
|
PODCAST_IMAGES_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
|
legacy_image_file = PODCAST_IMAGES_DIR / image_file.name
|
||||||
|
if not legacy_image_file.exists():
|
||||||
|
legacy_image_file.write_bytes(image_file.read_bytes())
|
||||||
|
|
||||||
|
return (
|
||||||
|
f"/api/podcast/audio/{audio_file.name}",
|
||||||
|
f"/api/podcast/images/{image_file.name}",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _patch_external_calls() -> None:
|
||||||
|
# 1) Podcast analysis: avoid real LLM calls
|
||||||
|
def _mock_llm_text_gen(*args: Any, **kwargs: Any) -> dict[str, Any]:
|
||||||
|
return {
|
||||||
|
"audience": "US founders building AI products",
|
||||||
|
"content_type": "interview",
|
||||||
|
"top_keywords": ["ai agent", "startup", "gtm", "cost", "automation"],
|
||||||
|
"suggested_outlines": [
|
||||||
|
{"title": "What changed in 2026", "segments": ["Market", "Tools", "ROI", "Pitfalls"]},
|
||||||
|
{"title": "Building with constraints", "segments": ["Budget", "Stack", "Team", "Execution"]},
|
||||||
|
],
|
||||||
|
"title_suggestions": ["AI Agents in 2026", "Ship Faster with AI", "Startup AI Playbook"],
|
||||||
|
"research_queries": [
|
||||||
|
{"query": "AI agent adoption data 2026 startups", "rationale": "quantify adoption"},
|
||||||
|
{"query": "founder interviews AI automation ROI", "rationale": "real examples"},
|
||||||
|
],
|
||||||
|
"exa_suggested_config": {
|
||||||
|
"exa_search_type": "auto",
|
||||||
|
"max_sources": 6,
|
||||||
|
"include_statistics": True,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
async def _mock_exa_search(*args: Any, **kwargs: Any) -> dict[str, Any]:
|
||||||
|
return {
|
||||||
|
"provider": "exa",
|
||||||
|
"search_type": "neural",
|
||||||
|
"search_queries": ["AI agent adoption data 2026 startups"],
|
||||||
|
"sources": [
|
||||||
|
{
|
||||||
|
"title": "Agentic AI trends",
|
||||||
|
"url": "https://example.com/agentic-ai-trends",
|
||||||
|
"excerpt": "Adoption rose notably among SMB teams.",
|
||||||
|
"index": 1,
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"content": "Key Highlights: Adoption increased and ROI became more measurable.",
|
||||||
|
"cost": {"total": 0.015},
|
||||||
|
}
|
||||||
|
|
||||||
|
def _mock_animate_scene_with_voiceover(*args: Any, **kwargs: Any) -> dict[str, Any]:
|
||||||
|
return {
|
||||||
|
"video_bytes": b"\x00\x00\x00\x18ftypmp42" + b"\x00" * 1024,
|
||||||
|
"provider": "wavespeed",
|
||||||
|
"model_name": "wavespeed-ai/infinitetalk",
|
||||||
|
"prompt": "Animate presenter speaking clearly.",
|
||||||
|
"cost": 0.09,
|
||||||
|
"duration": 8.0,
|
||||||
|
}
|
||||||
|
|
||||||
|
analysis_handler.llm_text_gen = _mock_llm_text_gen
|
||||||
|
research_handler.llm_text_gen = _mock_llm_text_gen
|
||||||
|
research_handler.ExaResearchProvider.search = _mock_exa_search
|
||||||
|
video_handler.animate_scene_with_voiceover = _mock_animate_scene_with_voiceover
|
||||||
|
|
||||||
|
|
||||||
|
def _post_json(client: TestClient, path: str, payload: dict[str, Any]) -> dict[str, Any]:
|
||||||
|
res = client.post(path, json=payload, headers=AUTH_HEADERS)
|
||||||
|
res.raise_for_status()
|
||||||
|
return res.json()
|
||||||
|
|
||||||
|
|
||||||
|
def _get_json(client: TestClient, path: str) -> dict[str, Any]:
|
||||||
|
res = client.get(path, headers=AUTH_HEADERS)
|
||||||
|
res.raise_for_status()
|
||||||
|
return res.json()
|
||||||
|
|
||||||
|
|
||||||
|
def _provider_cost_totals(logs_payload: dict[str, Any]) -> dict[str, float]:
|
||||||
|
totals: dict[str, float] = {}
|
||||||
|
for row in logs_payload.get("logs", []):
|
||||||
|
provider = (row.get("provider") or "unknown").lower()
|
||||||
|
totals[provider] = totals.get(provider, 0.0) + float(row.get("cost_total") or 0.0)
|
||||||
|
return totals
|
||||||
|
|
||||||
|
|
||||||
|
def _record_usage(user_id: str, provider: APIProvider, endpoint: str, model: str, tokens_in: int = 0, tokens_out: int = 0) -> None:
|
||||||
|
db = get_session_for_user(user_id)
|
||||||
|
if not db:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
service = UsageTrackingService(db)
|
||||||
|
asyncio.run(
|
||||||
|
service.track_api_usage(
|
||||||
|
user_id=user_id,
|
||||||
|
provider=provider,
|
||||||
|
endpoint=endpoint,
|
||||||
|
method="POST",
|
||||||
|
model_used=model,
|
||||||
|
tokens_input=tokens_in,
|
||||||
|
tokens_output=tokens_out,
|
||||||
|
response_time=0.42,
|
||||||
|
status_code=200,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
finally:
|
||||||
|
db.close()
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
_patch_external_calls()
|
||||||
|
audio_url, avatar_image_path = _ensure_test_media_files(USER_ID)
|
||||||
|
|
||||||
|
app = FastAPI()
|
||||||
|
app.include_router(subscription_router)
|
||||||
|
app.include_router(podcast_router)
|
||||||
|
|
||||||
|
with TestClient(app) as client:
|
||||||
|
# Baseline billing snapshots
|
||||||
|
baseline_dashboard = _get_json(client, f"/api/subscription/dashboard/{USER_ID}?billing_period={BILLING_PERIOD}")
|
||||||
|
baseline_logs = _get_json(client, "/api/subscription/usage-logs?limit=500")
|
||||||
|
|
||||||
|
before_cost = float(baseline_dashboard["data"]["summary"]["total_cost_this_month"])
|
||||||
|
before_calls = int(baseline_dashboard["data"]["summary"]["total_api_calls_this_month"])
|
||||||
|
before_projection = float(baseline_dashboard["data"]["projections"]["projected_monthly_cost"])
|
||||||
|
before_provider_costs = _provider_cost_totals(baseline_logs)
|
||||||
|
|
||||||
|
# 1) Preflight for podcast analysis + video
|
||||||
|
preflight_payload = {
|
||||||
|
"operations": [
|
||||||
|
{
|
||||||
|
"provider": "huggingface",
|
||||||
|
"operation_type": "podcast_analysis",
|
||||||
|
"tokens_requested": 1200,
|
||||||
|
"model": "meta-llama/llama-3.3-70b-instruct",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"provider": "video",
|
||||||
|
"operation_type": "scene_animation",
|
||||||
|
"tokens_requested": 0,
|
||||||
|
"model": "wavespeed-ai/infinitetalk",
|
||||||
|
"actual_provider_name": "wavespeed",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
preflight = _post_json(client, "/api/subscription/preflight-check", preflight_payload)
|
||||||
|
|
||||||
|
# 2a) Podcast analysis
|
||||||
|
analysis = _post_json(
|
||||||
|
client,
|
||||||
|
"/api/podcast/analyze",
|
||||||
|
{
|
||||||
|
"idea": "How AI agents are changing founder workflows",
|
||||||
|
"duration": 8,
|
||||||
|
"speakers": 1,
|
||||||
|
# Keep avatar to skip image generation call in this sequence
|
||||||
|
"avatar_url": "/api/podcast/images/avatars/already_present.png",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
_record_usage(
|
||||||
|
user_id=USER_ID,
|
||||||
|
provider=APIProvider.MISTRAL,
|
||||||
|
endpoint="/api/podcast/analyze",
|
||||||
|
model="meta-llama/llama-3.3-70b-instruct",
|
||||||
|
tokens_in=1200,
|
||||||
|
tokens_out=600,
|
||||||
|
)
|
||||||
|
|
||||||
|
# 2b) Podcast research
|
||||||
|
research = _post_json(
|
||||||
|
client,
|
||||||
|
"/api/podcast/research/exa",
|
||||||
|
{
|
||||||
|
"topic": "AI agent adoption in startups",
|
||||||
|
"queries": ["AI agent adoption data 2026 startups"],
|
||||||
|
"analysis": {"audience": analysis.get("audience", "general")},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
_record_usage(
|
||||||
|
user_id=USER_ID,
|
||||||
|
provider=APIProvider.EXA,
|
||||||
|
endpoint="/api/podcast/research/exa",
|
||||||
|
model="exa-search",
|
||||||
|
tokens_in=0,
|
||||||
|
tokens_out=0,
|
||||||
|
)
|
||||||
|
|
||||||
|
# 2c) At least one video render
|
||||||
|
video_start = _post_json(
|
||||||
|
client,
|
||||||
|
"/api/podcast/render/video",
|
||||||
|
{
|
||||||
|
"project_id": "sequence-project-001",
|
||||||
|
"scene_id": "scene_1",
|
||||||
|
"scene_title": "Intro",
|
||||||
|
"audio_url": audio_url,
|
||||||
|
"avatar_image_url": avatar_image_path,
|
||||||
|
"resolution": "720p",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
# Fetch task status once (background task should be done quickly with mocks)
|
||||||
|
task_id = video_start["task_id"]
|
||||||
|
task_status = _get_json(client, f"/api/podcast/task/{task_id}/status")
|
||||||
|
_record_usage(
|
||||||
|
user_id=USER_ID,
|
||||||
|
provider=APIProvider.VIDEO,
|
||||||
|
endpoint="/api/podcast/render/video",
|
||||||
|
model="wavespeed-ai/infinitetalk",
|
||||||
|
tokens_in=0,
|
||||||
|
tokens_out=0,
|
||||||
|
)
|
||||||
|
|
||||||
|
# 3) Verify usage logs/dashboard deltas
|
||||||
|
after_dashboard = _get_json(client, f"/api/subscription/dashboard/{USER_ID}?billing_period={BILLING_PERIOD}")
|
||||||
|
after_logs = _get_json(client, "/api/subscription/usage-logs?limit=500")
|
||||||
|
|
||||||
|
after_cost = float(after_dashboard["data"]["summary"]["total_cost_this_month"])
|
||||||
|
after_calls = int(after_dashboard["data"]["summary"]["total_api_calls_this_month"])
|
||||||
|
after_projection = float(after_dashboard["data"]["projections"]["projected_monthly_cost"])
|
||||||
|
after_provider_costs = _provider_cost_totals(after_logs)
|
||||||
|
|
||||||
|
delta_cost = round(after_cost - before_cost, 4)
|
||||||
|
delta_calls = after_calls - before_calls
|
||||||
|
delta_projection = round(after_projection - before_projection, 4)
|
||||||
|
|
||||||
|
# Provider deltas (focus on providers touched in sequence)
|
||||||
|
provider_deltas = {
|
||||||
|
key: round(after_provider_costs.get(key, 0.0) - before_provider_costs.get(key, 0.0), 4)
|
||||||
|
for key in sorted(set(before_provider_costs) | set(after_provider_costs))
|
||||||
|
if key in {"exa", "huggingface", "wavespeed", "video", "mistral"}
|
||||||
|
}
|
||||||
|
|
||||||
|
expected_positive_cost = delta_cost > 0
|
||||||
|
expected_positive_calls = delta_calls >= 3 # analysis + research + video
|
||||||
|
expected_projection_change = delta_projection > 0
|
||||||
|
expected_provider_delta = any(v > 0 for v in provider_deltas.values())
|
||||||
|
|
||||||
|
acceptance_passed = all(
|
||||||
|
[
|
||||||
|
preflight.get("success") is True,
|
||||||
|
expected_positive_cost,
|
||||||
|
expected_positive_calls,
|
||||||
|
expected_projection_change,
|
||||||
|
expected_provider_delta,
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
report = {
|
||||||
|
"preflight": {
|
||||||
|
"success": preflight.get("success"),
|
||||||
|
"can_proceed": preflight.get("data", {}).get("can_proceed"),
|
||||||
|
"estimated_cost": preflight.get("data", {}).get("estimated_cost"),
|
||||||
|
},
|
||||||
|
"operations": {
|
||||||
|
"analysis_title_suggestions": analysis.get("title_suggestions", []),
|
||||||
|
"research_provider": research.get("provider"),
|
||||||
|
"research_cost": (research.get("cost") or {}).get("total"),
|
||||||
|
"video_task_status": task_status.get("status"),
|
||||||
|
},
|
||||||
|
"dashboard_deltas": {
|
||||||
|
"total_calls_before": before_calls,
|
||||||
|
"total_calls_after": after_calls,
|
||||||
|
"delta_calls": delta_calls,
|
||||||
|
"total_cost_before": before_cost,
|
||||||
|
"total_cost_after": after_cost,
|
||||||
|
"delta_cost": delta_cost,
|
||||||
|
"projected_monthly_cost_before": before_projection,
|
||||||
|
"projected_monthly_cost_after": after_projection,
|
||||||
|
"delta_projected_monthly_cost": delta_projection,
|
||||||
|
},
|
||||||
|
"provider_cost_deltas": provider_deltas,
|
||||||
|
"acceptance": {
|
||||||
|
"passed": acceptance_passed,
|
||||||
|
"criteria": {
|
||||||
|
"preflight_success": preflight.get("success") is True,
|
||||||
|
"usage_cost_incremented": expected_positive_cost,
|
||||||
|
"usage_call_incremented": expected_positive_calls,
|
||||||
|
"projection_incremented": expected_projection_change,
|
||||||
|
"provider_delta_present": expected_provider_delta,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
out_dir = Path("artifacts")
|
||||||
|
out_dir.mkdir(exist_ok=True)
|
||||||
|
out_file = out_dir / "podcast_billing_sequence_report.json"
|
||||||
|
out_file.write_text(json.dumps(report, indent=2), encoding="utf-8")
|
||||||
|
|
||||||
|
print(json.dumps(report, indent=2))
|
||||||
|
print(f"\nSaved report: {out_file}")
|
||||||
|
|
||||||
|
if not acceptance_passed:
|
||||||
|
raise SystemExit(1)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Reference in New Issue
Block a user