@thierrynakoa/fire-flow 12.2.1 → 13.0.1
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/CREDITS.md +25 -0
- package/DOMINION-FLOW-OVERVIEW.md +182 -38
- package/README.md +399 -455
- package/TROUBLESHOOTING.md +264 -264
- package/agents/fire-debugger.md +54 -0
- package/agents/fire-executor.md +1610 -1033
- package/agents/fire-fact-checker.md +1 -1
- package/agents/fire-planner.md +85 -17
- package/agents/fire-project-researcher.md +1 -1
- package/agents/fire-researcher.md +4 -22
- package/agents/{fire-phoenix-analyst.md → fire-resurrection-analyst.md} +394 -394
- package/agents/fire-reviewer.md +552 -499
- package/agents/fire-verifier.md +114 -19
- package/bin/cli.js +18 -101
- package/commands/fire-0-orient.md +2 -2
- package/commands/fire-1a-new.md +50 -15
- package/commands/fire-1c-setup.md +33 -5
- package/commands/fire-1d-discuss.md +87 -1
- package/commands/fire-2-plan.md +556 -527
- package/commands/fire-3-execute.md +2046 -1356
- package/commands/fire-4-verify.md +975 -906
- package/commands/fire-5-handoff.md +46 -5
- package/commands/fire-6-resume.md +2 -31
- package/commands/fire-add-new-skill.md +138 -19
- package/commands/fire-autonomous.md +14 -2
- package/commands/fire-complete-milestone.md +1 -1
- package/commands/fire-cost.md +179 -183
- package/commands/fire-debug.md +1 -6
- package/commands/fire-loop-resume.md +2 -2
- package/commands/fire-loop-stop.md +1 -1
- package/commands/fire-loop.md +2 -15
- package/commands/fire-map-codebase.md +1 -1
- package/commands/fire-migrate-database.md +548 -0
- package/commands/fire-new-milestone.md +1 -1
- package/commands/fire-reflect.md +1 -2
- package/commands/fire-research.md +142 -21
- package/commands/{fire-phoenix.md → fire-resurrect.md} +859 -603
- package/commands/fire-scaffold.md +297 -0
- package/commands/fire-search.md +1 -2
- package/commands/fire-security-scan.md +483 -484
- package/commands/fire-setup.md +359 -0
- package/commands/fire-skill.md +770 -0
- package/commands/fire-skills-diff.md +506 -506
- package/commands/fire-skills-history.md +388 -388
- package/commands/fire-skills-rollback.md +7 -7
- package/commands/fire-skills-sync.md +470 -470
- package/commands/fire-test.md +5 -5
- package/commands/fire-todos.md +1 -1
- package/commands/fire-update.md +5 -5
- package/commands/fire-validate-skills.md +282 -0
- package/commands/fire-vuln-scan.md +492 -493
- package/hooks/run-hook.sh +8 -8
- package/hooks/run-session-end.sh +7 -7
- package/hooks/session-end.sh +90 -90
- package/hooks/session-start.sh +1 -1
- package/package.json +4 -24
- package/plugin.json +7 -7
- package/references/autonomy-levels.md +235 -0
- package/references/behavioral-directives.md +95 -3
- package/references/blocker-tracking.md +1 -1
- package/references/circuit-breaker.md +93 -2
- package/references/context-engineering.md +227 -9
- package/references/honesty-protocols.md +70 -1
- package/references/issue-to-pr-pipeline.md +149 -150
- package/references/metrics-and-trends.md +1 -2
- package/references/research-improvements.md +4 -108
- package/references/sdlc-mapping.md +73 -0
- package/references/state-machine.md +151 -0
- package/skills-library/AVAILABLE_TOOLS_REFERENCE.md +333 -0
- package/skills-library/SKILLS-INDEX.md +57 -558
- package/skills-library/SKILLS_LIBRARY_INDEX.md +532 -0
- package/skills-library/_general/api-patterns/api-field-name-mismatch.md +107 -0
- package/skills-library/_general/api-patterns/streaming-command-timeout.md +122 -0
- package/skills-library/_general/api-patterns/streaming-proxy-cors-bypass.md +102 -0
- package/skills-library/_general/automation/settings-gui-generator.md +172 -0
- package/skills-library/_general/database-solutions/data-type-mapping-reference.md +181 -0
- package/skills-library/_general/database-solutions/mysql-limit-offset-string-coercion.md +102 -0
- package/skills-library/_general/database-solutions/mysql-to-pg-migration.md +195 -0
- package/skills-library/_general/database-solutions/orm-schema-portability.md +193 -0
- package/skills-library/_general/database-solutions/persistent-analysis-storage.md +207 -0
- package/skills-library/_general/database-solutions/pg-to-mysql-schema-migration-methodology.md +190 -0
- package/skills-library/_general/database-solutions/sql-dialect-compatibility-matrix.md +306 -0
- package/skills-library/_general/database-solutions/sqlite-to-pg-migration.md +219 -0
- package/skills-library/_general/frontend/canvas-bubble-animation-grouping.md +270 -0
- package/skills-library/_general/frontend/color-token-migration.md +112 -0
- package/skills-library/_general/frontend/framer-motion-layoutid-grouping.md +150 -0
- package/skills-library/_general/frontend/pyqt6-settings-dialog.md +191 -0
- package/skills-library/_general/frontend/react-flow-animated-layout-switching.md +101 -0
- package/skills-library/_general/frontend/react-hooks-order-debugging.md +141 -0
- package/skills-library/_general/frontend/redux-localstorage-auth-desync.md +126 -0
- package/skills-library/_general/frontend/safari-csp-theme-color-debugging.md +124 -0
- package/skills-library/_general/frontend/safari-sw-cache-poisoning.md +138 -0
- package/skills-library/_general/frontend/svg-sparkline-no-charting-library.md +131 -0
- package/skills-library/_general/growth-marketing/oss-daily-growth-intelligence.md +224 -0
- package/skills-library/_general/integrations/claude-code-local-mcp-integration.md +250 -0
- package/skills-library/_general/integrations/mcp-composite-tool-orchestration.md +200 -0
- package/skills-library/_general/methodology/AGENT_SDK_STANDALONE_TOOLING.md +181 -0
- package/skills-library/_general/methodology/AGENT_TEAMS_GUIDE.md +169 -0
- package/skills-library/_general/methodology/ALAS_STATEFUL_EXECUTION.md +207 -0
- package/skills-library/_general/methodology/AUTO_REVIEWER_SUBAGENT.md +211 -0
- package/skills-library/_general/methodology/CONSISTENCY_CHECK_AMBIGUITY_GATE.md +96 -0
- package/skills-library/_general/methodology/DEAD_ENDS_SHELF.md +4 -4
- package/skills-library/_general/methodology/DISTILL_NOT_DUMP.md +108 -0
- package/skills-library/_general/methodology/EXECUTION_PROGRESS_MONITOR.md +157 -0
- package/skills-library/_general/methodology/HIERARCHICAL_REVIEW_MARS.md +122 -0
- package/skills-library/_general/methodology/MCP_INTER_AGENT_BRIDGE.md +207 -0
- package/skills-library/_general/methodology/MERMAID_WIZARD_DIAGRAMS.md +77 -0
- package/skills-library/_general/methodology/MISSING_DIMENSION_DETECTOR.md +89 -0
- package/skills-library/_general/methodology/MULTI_AGENT_COORDINATION.md +397 -0
- package/skills-library/_general/methodology/OBSERVATION_MASKING.md +100 -0
- package/skills-library/_general/methodology/PHOENIX_REBUILD_METHODOLOGY.md +82 -11
- package/skills-library/_general/methodology/REVIEW_BACKTRACK_PANEL.md +140 -0
- package/skills-library/_general/methodology/REVIEW_FIX_LOOP.md +117 -0
- package/skills-library/_general/methodology/VOTING_VERDICT_ARBITRATION.md +155 -0
- package/skills-library/_general/methodology/ZERO_FRICTION_CLI_SETUP.md +2 -2
- package/skills-library/_general/methodology/dead-code-activation.md +123 -0
- package/skills-library/_general/methodology/debug-swarm-researcher-escape-hatch.md +240 -240
- package/skills-library/_general/methodology/shell-autonomous-loop-fixplan.md +1 -1
- package/skills-library/_general/patterns-standards/GOF_DESIGN_PATTERNS_FOR_AI_AGENTS.md +5 -5
- package/skills-library/_general/patterns-standards/cascading-failure-diagnosis.md +119 -0
- package/skills-library/_general/patterns-standards/domain-specific-layout-algorithms.md +209 -0
- package/skills-library/_general/patterns-standards/python-desktop-app-architecture.md +399 -0
- package/skills-library/_general/patterns-standards/realtime-monitoring-dashboard.md +457 -0
- package/skills-library/_general/patterns-standards/togglable-processing-pipeline.md +169 -0
- package/skills-library/_general/performance/liveclock-extraction.md +112 -0
- package/skills-library/_general/performance/ref-based-canvas-animation.md +117 -0
- package/skills-library/_general/performance/use-visible-interval.md +131 -0
- package/skills-library/_general/testing/playwright-firefox-withcredentials-auth-issue.md +104 -0
- package/skills-library/_quarantine/README.md +30 -0
- package/skills-library/api-patterns/BROADCAST_SCHEDULER_SHARED_EXECUTE_FUNCTION.md +150 -0
- package/skills-library/api-patterns/ERROR_RESPONSE_STANDARDS.md +145 -0
- package/skills-library/api-patterns/EXPRESS_ROUTE_ORDERING_MIDDLEWARE_INTERCEPTION.md +326 -0
- package/skills-library/api-patterns/PAGINATION_PATTERNS.md +137 -0
- package/skills-library/api-patterns/PODCAST_PROGRESS_TRACKING_THREE_ROOT_CAUSES.md +277 -0
- package/skills-library/api-patterns/RATE_LIMITING_TOGGLE.md +155 -0
- package/skills-library/api-patterns/graphql-content-queries.md +708 -0
- package/skills-library/appointment-scheduler-design.md +423 -0
- package/skills-library/automation/AUTO_POPULATE_COMPLETE_GUIDE.md +631 -0
- package/skills-library/automation/CC_WORKFLOW_STUDIO.md +83 -0
- package/skills-library/automation/CLAUDE_CODE_SWARM_MODE.md +95 -0
- package/skills-library/automation/DAEMON_TRIGGER_FILE_IPC.md +195 -0
- package/skills-library/automation/scheduled-content-publishing.md +608 -0
- package/skills-library/awesome-workflows/Blogging-Platform-Instructions/view_commands.md +25 -0
- package/skills-library/awesome-workflows/CREDENTIAL-SECURITY-WORKFLOW.md +109 -0
- package/skills-library/awesome-workflows/DEBUGGING-WORKFLOW.md +124 -0
- package/skills-library/awesome-workflows/Design-Review-Workflow/README.md +31 -0
- package/skills-library/awesome-workflows/Design-Review-Workflow/design-principles-example.md +129 -0
- package/skills-library/awesome-workflows/Design-Review-Workflow/design-review-agent.md +107 -0
- package/skills-library/awesome-workflows/Design-Review-Workflow/design-review-claude-md-snippet.md +24 -0
- package/skills-library/awesome-workflows/Design-Review-Workflow/design-review-slash-command.md +38 -0
- package/skills-library/awesome-workflows/PARALLEL-RESEARCH-WORKFLOW.md +89 -0
- package/skills-library/awesome-workflows/PHASE-EXECUTION-WORKFLOW.md +97 -0
- package/skills-library/awesome-workflows/SESSION-HANDOFF-WORKFLOW.md +116 -0
- package/skills-library/cms-patterns/content-branch-preview.md +515 -0
- package/skills-library/cms-patterns/inline-visual-editing.md +666 -0
- package/skills-library/cms-patterns/mdx-component-content.md +649 -0
- package/skills-library/cms-patterns/media-manager-abstraction.md +827 -0
- package/skills-library/cms-patterns/schema-driven-form-generator.md +838 -0
- package/skills-library/complexity-metrics/complexity-divider.md +707 -0
- package/skills-library/complexity-metrics/work-with-complexity.md +193 -0
- package/skills-library/creative-multimedia/animation-stack-guide.md +577 -0
- package/skills-library/creative-multimedia/audio-enhancement-pipeline.md +625 -0
- package/skills-library/creative-multimedia/content-repurposing-pipeline.md +1146 -0
- package/skills-library/creative-multimedia/data-visualization-generator.md +862 -0
- package/skills-library/creative-multimedia/doc-to-podcast-pipeline.md +2184 -0
- package/skills-library/creative-multimedia/ffmpeg-command-generator.md +405 -0
- package/skills-library/creative-multimedia/image-optimization-pipeline.md +605 -0
- package/skills-library/creative-multimedia/multi-format-content-generator.md +1759 -0
- package/skills-library/creative-multimedia/og-image-generator.md +635 -0
- package/skills-library/creative-multimedia/podcast-audio-composition.md +1355 -0
- package/skills-library/creative-multimedia/podcast-quality-evaluation.md +1452 -0
- package/skills-library/creative-multimedia/podcast-script-generation.md +1841 -0
- package/skills-library/creative-multimedia/svg-generation.md +750 -0
- package/skills-library/creative-multimedia/text-to-speech-provider-selector.md +1414 -0
- package/skills-library/creative-multimedia/transcription-pipeline-selector.md +677 -0
- package/skills-library/creative-multimedia/video-streaming-setup.md +559 -0
- package/skills-library/database-solutions/AI_RESPONSE_DATABASE_CACHING.md +520 -0
- package/skills-library/database-solutions/CONDITIONAL_SQL_MIGRATION_PATTERN.md +119 -0
- package/skills-library/database-solutions/DATABASE_COLUMN_NAME_MISMATCH.md +393 -0
- package/skills-library/database-solutions/DATABASE_SCHEMA.md +394 -0
- package/skills-library/database-solutions/DATABASE_SCHEMA_VERIFICATION_GUIDE.md +348 -0
- package/skills-library/database-solutions/DATABASE_STRATEGY.md +71 -0
- package/skills-library/database-solutions/ES_MODULE_SEED_SCRIPT_PATTERN.md +52 -0
- package/skills-library/database-solutions/MIGRATION_GUIDE.md +3 -0
- package/skills-library/database-solutions/PLPGSQL_VARIABLE_CONFLICT_FIX.md +208 -0
- package/skills-library/database-solutions/POSTGRESQL_JSONB_DOUBLE_STRINGIFY_FIX.md +245 -0
- package/skills-library/database-solutions/POSTGRESQL_LICENSE_TABLE_DESIGN.md +393 -0
- package/skills-library/database-solutions/POSTGRESQL_UUID_DOCUMENT_RAG_DUAL_SCOPE.md +732 -0
- package/skills-library/database-solutions/POSTGRES_SQL_TEMPLATE_BINDING_ERROR.md +240 -0
- package/skills-library/database-solutions/PRISMA_DB_PUSH_DATA_LOSS_PREVENTION.md +141 -0
- package/skills-library/database-solutions/PRODUCTION_QUERY_OPTIMIZATION_RESTART_FIX.md +389 -0
- package/skills-library/database-solutions/RLS_SECURITY_GUIDE.md +107 -0
- package/skills-library/database-solutions/SCHEMA_ENHANCEMENTS_GUIDE.md +373 -0
- package/skills-library/database-solutions/SCHEMA_MIGRATION_GUIDE.md +368 -0
- package/skills-library/database-solutions/SCHEMA_VERIFICATION_QUICK_REFERENCE.md +104 -0
- package/skills-library/database-solutions/ai-erd-generator.md +1213 -0
- package/skills-library/database-solutions/content-publishing-states.md +631 -0
- package/skills-library/database-solutions/database-schema-designer.md +522 -0
- package/skills-library/database-solutions/er-diagram-components.md +569 -0
- package/skills-library/database-solutions/er-to-ddl-mapping.md +1405 -0
- package/skills-library/database-solutions/erd-creator-textbook-research.md +433 -0
- package/skills-library/database-solutions/erd-react-flow-architecture.md +1965 -0
- package/skills-library/database-solutions/mariadb-aggregate-function-replacement.md +145 -0
- package/skills-library/database-solutions/normalization-validator.md +778 -0
- package/skills-library/database-solutions/postgres-full-text-search-content.md +494 -0
- package/skills-library/database-solutions/postgresql-to-mysql-runtime-translation.md +286 -0
- package/skills-library/database-solutions/regex-alternation-ordering-sql-types.md +92 -0
- package/skills-library/database-solutions/reserved-word-context-aware-quoting.md +142 -0
- package/skills-library/database-solutions/sql-ddl-generator.md +756 -0
- package/skills-library/database-solutions/supabase-connection-pooler-fix.md +102 -0
- package/skills-library/deployment-security/CPANEL_NODE_DEPLOYMENT.md +166 -0
- package/skills-library/deployment-security/DEPLOYMENT.md +275 -0
- package/skills-library/deployment-security/DEPLOYMENT_CHECKLIST.md +363 -0
- package/skills-library/deployment-security/DEPLOYMENT_PLAN.md +669 -0
- package/skills-library/deployment-security/KNEX_DATABASE_ABSTRACTION.md +444 -0
- package/skills-library/deployment-security/LICENSE_KEY_SYSTEM.md +206 -0
- package/skills-library/deployment-security/NODE18_DEPENDENCY_COMPATIBILITY.md +284 -0
- package/skills-library/deployment-security/PHP_INSTALLER_WIZARD_GUIDE.md +315 -0
- package/skills-library/deployment-security/PM2_ENVIRONMENT_VARIABLE_CACHING.md +256 -0
- package/skills-library/deployment-security/PM2_MEMORY_EXHAUSTION_FIX.md +370 -0
- package/skills-library/deployment-security/PRODUCTION_DEPLOYMENT_GUIDE.md +592 -0
- package/skills-library/deployment-security/PRODUCTION_HARDENING_DOCUMENTATION.md +307 -0
- package/skills-library/deployment-security/PRODUCTION_RECOVERY_CHERRY_PICK_PATTERN.md +202 -0
- package/skills-library/deployment-security/PYINSTALLER_CUDA_WHISPER_BUNDLING.md +236 -0
- package/skills-library/deployment-security/SECURITY.md +41 -0
- package/skills-library/deployment-security/SMTP_SSL_HOSTNAME_MISMATCH_SHARED_HOSTING.md +220 -0
- package/skills-library/deployment-security/SPA_SEO_OPTIMIZATION_CPANEL.md +200 -0
- package/skills-library/deployment-security/SUPABASE_EDGE_FUNCTIONS.md +338 -0
- package/skills-library/deployment-security/VERCEL_GITHUB_DEPLOYMENT_GUIDE.md +858 -0
- package/skills-library/deployment-security/VPS_DEPLOYMENT_READINESS.md +356 -0
- package/skills-library/deployment-security/deployment-changes-not-applying.md +241 -0
- package/skills-library/deployment-security/env-file-management-production-local.md +203 -0
- package/skills-library/deployment-security/express-secure-file-downloads.md +413 -0
- package/skills-library/deployment-security/react-production-deployment-desktop-guide.md +2011 -0
- package/skills-library/deployment-security/self-hosted-supabase-coolify-guide.md +1684 -0
- package/skills-library/deployment-security/unique-features-ai-strategy-plaid-security.md +1613 -0
- package/skills-library/deployment-security/vps-deployment.md +135 -0
- package/skills-library/document-processing/WORD_EXPORT_MARKDOWN_FORMATTING.md +482 -0
- package/skills-library/document-processing/document-ai-landingai-integration.md +677 -0
- package/skills-library/document-processing/express-secure-file-downloads-mern.md +413 -0
- package/skills-library/document-processing/express-secure-file-downloads.md +413 -0
- package/skills-library/document-processing/md-to-word-converter.md +318 -0
- package/skills-library/document-processing/pdf-forms-integration/README.md +101 -0
- package/skills-library/document-processing/pdf-forms-integration/SKILL.md +662 -0
- package/skills-library/ecommerce/ADMIN_PRODUCTS_GUIDE.md +428 -0
- package/skills-library/ecommerce/ECOMMERCE_API_REFERENCE.md +776 -0
- package/skills-library/ecommerce/ECOMMERCE_COMPLETION_SUMMARY.md +673 -0
- package/skills-library/ecommerce/ECOMMERCE_IMPLEMENTATION_GUIDE.md +729 -0
- package/skills-library/ecommerce/ECOMMERCE_QUICK_REFERENCE.md +521 -0
- package/skills-library/ecommerce/ECOMMERCE_TESTING_CHECKLIST.md +565 -0
- package/skills-library/ecommerce/ECOMMERCE_WORKFLOW_GUIDE.md +1059 -0
- package/skills-library/ecommerce/PRODUCT_CREATION_EXPANDED.md +522 -0
- package/skills-library/ecommerce/agentic-commerce-protocol.md +203 -0
- package/skills-library/ecommerce/cart-abandonment-recovery.md +236 -0
- package/skills-library/ecommerce/cart-architecture-patterns.md +300 -0
- package/skills-library/ecommerce/cart-item-count-indicator.md +264 -0
- package/skills-library/ecommerce/checkout-ux-conversion.md +227 -0
- package/skills-library/ecommerce/composable-commerce-selection.md +166 -0
- package/skills-library/ecommerce/ecommerce-analytics-patterns.md +167 -0
- package/skills-library/ecommerce/fraud-detection-patterns.md +179 -0
- package/skills-library/ecommerce/inventory-stock-management.md +270 -0
- package/skills-library/ecommerce/order-saga-state-machine.md +336 -0
- package/skills-library/ecommerce/payment-provider-abstraction.md +245 -0
- package/skills-library/ecommerce/pci-compliance-checklist.md +192 -0
- package/skills-library/ecommerce/refund-chargeback-handling.md +177 -0
- package/skills-library/ecommerce/shipping-carrier-integration.md +218 -0
- package/skills-library/ecommerce/webhook-idempotency-patterns.md +253 -0
- package/skills-library/excalidraw-diagrams/.github/workflows/ci.yml +558 -0
- package/skills-library/excalidraw-diagrams/.github/workflows/prompt-gallery.yml +448 -0
- package/skills-library/excalidraw-diagrams/.github/workflows/release.yml +42 -0
- package/skills-library/excalidraw-diagrams/.github/workflows/test-reusable-ci.yml +25 -0
- package/skills-library/excalidraw-diagrams/CLAUDE.md +57 -0
- package/skills-library/excalidraw-diagrams/LICENSE +21 -0
- package/skills-library/excalidraw-diagrams/README.md +178 -0
- package/skills-library/excalidraw-diagrams/SKILL.md +715 -0
- package/skills-library/form-solutions/BUTTON_TYPE_FORM_SUBMISSION.md +336 -0
- package/skills-library/form-solutions/FILLABLE_PDF_IMPLEMENTATION.md +226 -0
- package/skills-library/form-solutions/SURVEYJS_QUESTIONNAIRE_SYSTEM.md +367 -0
- package/skills-library/form-solutions/tiptap-minimal-setup.md +690 -0
- package/skills-library/frontend/scholarly-classification-bubble-map.md +149 -0
- package/skills-library/infrastructure/ci-cd-pipeline-builder.md +517 -0
- package/skills-library/infrastructure/observability-designer.md +264 -0
- package/skills-library/infrastructure/performance-profiler.md +621 -0
- package/skills-library/installer-wizard-patterns.md +249 -0
- package/skills-library/integrations/CLAUDE_CODE_TOKEN_ANALYTICS.md +160 -0
- package/skills-library/integrations/CONFIGURABLE_AI_PROVIDER_SELECTION.md +728 -0
- package/skills-library/integrations/SOCKET_IO_BROADCAST_ALL_VS_ROOM.md +141 -0
- package/skills-library/integrations/VIRTUAL_MEETINGS_IMPLEMENTATION.md +374 -0
- package/skills-library/integrations/WORDPRESS_LEARNDASH_DATA_RECOVERY.md +53 -0
- package/skills-library/integrations/YOUTUBE_API_SETUP.md +141 -0
- package/skills-library/integrations/YOUTUBE_BOOKMARKING_EXPLANATION.md +252 -0
- package/skills-library/integrations/YOUTUBE_BOOKMARKING_SOLUTION.md +268 -0
- package/skills-library/integrations/YOUTUBE_OAUTH_SETUP_GUIDE.md +200 -0
- package/skills-library/integrations/YOUTUBE_VIDEO_FIX_COMPLETE.md +192 -0
- package/skills-library/integrations/ai-ml/GEMINI_AI_RAG_PIPELINE_COMPLETE_GUIDE.md +195 -0
- package/skills-library/integrations/ai-ml/GEMINI_IMAGE_GENERATION_SETUP.md +64 -0
- package/skills-library/integrations/cloudflare/cloudflare-turnstile-debugging.md +202 -0
- package/skills-library/integrations/cloudflare/cloudflare-turnstile-implementation.md +476 -0
- package/skills-library/integrations/cloudflare-turnstile-debugging.md +202 -0
- package/skills-library/integrations/cloudflare-turnstile-implementation.md +476 -0
- package/skills-library/integrations/ghost-creator-monetization-pattern.md +454 -0
- package/skills-library/integrations/headless-cms-architecture.md +484 -0
- package/skills-library/integrations/headless-cms-stack-selection.md +183 -0
- package/skills-library/integrations/payload-cms-patterns.md +674 -0
- package/skills-library/integrations/realtimestt-openwakeword-cuda-windows.md +229 -0
- package/skills-library/integrations/rss-podcast-integration.md +300 -0
- package/skills-library/integrations/wordpress/WORDPRESS_LEARNDASH_DATA_RECOVERY.md +53 -0
- package/skills-library/integrations/youtube/YOUTUBE_API_SETUP.md +141 -0
- package/skills-library/integrations/youtube/YOUTUBE_BOOKMARKING_EXPLANATION.md +252 -0
- package/skills-library/integrations/youtube/YOUTUBE_BOOKMARKING_SOLUTION.md +268 -0
- package/skills-library/integrations/youtube/YOUTUBE_OAUTH_SETUP_GUIDE.md +200 -0
- package/skills-library/integrations/youtube/YOUTUBE_VIDEO_FIX_COMPLETE.md +192 -0
- package/skills-library/marketing/campaign-analytics.md +97 -0
- package/skills-library/marketing/content-creator.md +105 -0
- package/skills-library/marketing/marketing-strategy-pmm.md +94 -0
- package/skills-library/marketing/social-media-analyzer.md +81 -0
- package/skills-library/methodology/ADVANCED_ORCHESTRATION_PATTERNS.md +401 -0
- package/skills-library/methodology/AGENT_SELF_IMPROVEMENT_LOOP.md +179 -0
- package/skills-library/methodology/BREATH_BASED_PARALLEL_EXECUTION.md +1 -1
- package/skills-library/methodology/CLEANSING_CYCLE.md +358 -0
- package/skills-library/methodology/CONFIDENCE_ANNOTATION_PATTERN.md +143 -0
- package/skills-library/methodology/CRITICAL_PATTERNS_DOCUMENTATION_COMPLETE.md +204 -0
- package/skills-library/methodology/DELIVERABLES_SUMMARY.md +341 -0
- package/skills-library/methodology/DIFFICULTY_AWARE_AGENT_ROUTING.md +252 -0
- package/skills-library/methodology/EVOLUTIONARY_SKILL_SYNTHESIS.md +219 -0
- package/skills-library/methodology/GLOMERULUS_DECISION_GATE.md +223 -0
- package/skills-library/methodology/HIBERNATION_SYSTEM.md +231 -0
- package/skills-library/methodology/INSTRUMENTATION_OVER_RESTRICTION.md +192 -0
- package/skills-library/methodology/MASTER_COMPLETION_SUMMARY.md +444 -0
- package/skills-library/methodology/MASTER_SESSION_COMPLETION.md +743 -0
- package/skills-library/methodology/MERN_QUICK_REFERENCE.md +358 -0
- package/skills-library/methodology/ORGAN_AGENT_MAPPING.md +177 -0
- package/skills-library/methodology/PARALLEL_WAVE_BASED_REFACTORING.md +440 -0
- package/skills-library/methodology/QUICK_REFERENCE.md +358 -0
- package/skills-library/methodology/SDFT_ONPOLICY_SELF_DISTILLATION.md +186 -0
- package/skills-library/methodology/SELF_QUESTIONING_TASK_GENERATION.md +270 -0
- package/skills-library/methodology/SESSION_COMPLETION_SUMMARY.md +304 -0
- package/skills-library/methodology/SESSION_SUMMARY.md +432 -0
- package/skills-library/methodology/WARRIOR_WORKFLOW_DEBUGGING_PROTOCOL.md +252 -0
- package/skills-library/methodology/tech-debt-tracker.md +570 -0
- package/skills-library/parallel-debug/SKILL.md +60 -0
- package/skills-library/patterns-standards/API_PATTERN_FIX_SUMMARY.md +236 -0
- package/skills-library/patterns-standards/BATCH_OPERATIONS_WITH_PROGRESS_MODAL.md +362 -0
- package/skills-library/patterns-standards/CRITICAL_CODING_PATTERNS.md +639 -0
- package/skills-library/patterns-standards/DARK_MODE_MODAL_VISIBILITY.md +258 -0
- package/skills-library/patterns-standards/ERROR_RESILIENCE_IMPLEMENTATION.md +375 -0
- package/skills-library/patterns-standards/ES_MODULE_IMPORT_HOISTING_DOTENV.md +298 -0
- package/skills-library/patterns-standards/NESTED_BACKDROP_FILTER_CSS_ARTIFACT_FIX.md +76 -0
- package/skills-library/patterns-standards/ORDERED_DETECTOR_PIPELINE_GRACEFUL_FALLBACK.md +333 -0
- package/skills-library/patterns-standards/PHASE_IMPORT_ERROR_DEBUGGING.md +271 -0
- package/skills-library/patterns-standards/PYNPUT_GLOBAL_HOTKEY_VK_MATCHING.md +252 -0
- package/skills-library/patterns-standards/REACT_USEEFFECT_CASCADE_RESET_FIX.md +132 -0
- package/skills-library/patterns-standards/SUBMENU_HOVER_DROPDOWN_PATTERN.md +225 -0
- package/skills-library/patterns-standards/TAILWIND_TEXT_VISIBILITY_OVERRIDE.md +322 -0
- package/skills-library/patterns-standards/THEME_AWARE_CSS_VARIABLES_PATTERN.md +209 -0
- package/skills-library/patterns-standards/THEME_USER_OBJECT_PROPERTY_NAMING.md +194 -0
- package/skills-library/patterns-standards/TOOLTIP_BLOCKING_CLICKS_FIX.md +267 -0
- package/skills-library/patterns-standards/claude-code-plugin-structure.md +235 -0
- package/skills-library/patterns-standards/react-i18next-setup.md +429 -0
- package/skills-library/patterns-standards/thesys-c1-generative-ui-integration.md +967 -0
- package/skills-library/plugin-development/CLAUDE_CODE_COMMAND_REGISTRATION_SILENT_FAILURE.md +315 -0
- package/skills-library/plugin-development/plugin-command-namespace-vs-global.md +390 -0
- package/skills-library/plugin-development/plugin-doc-auto-generation.md +172 -0
- package/skills-library/security/GITHUB_REPO_SECURITY_AUDIT.md +115 -0
- package/skills-library/security/admin-deletion-safety.md +396 -0
- package/skills-library/security/application-vuln-patterns.md +477 -0
- package/skills-library/security/env-secrets-manager.md +686 -0
- package/skills-library/security/secure-ai-application-templates.md +347 -0
- package/skills-library/security/sql-injection-prevention-postgresjs.md +151 -0
- package/skills-library/supabase-connection-pooler-fix.md +102 -0
- package/skills-library/system-context/POWERSHELL_BASH_INTEROP.md +82 -0
- package/skills-library/system-context/SERVICE_LIFECYCLE_MANAGEMENT.md +119 -0
- package/skills-library/system-context/SKILL.md +40 -0
- package/skills-library/system-context/WINDOWS_DEV_ENVIRONMENT.md +73 -0
- package/skills-library/testing/E2E_PLAYWRIGHT_PATTERNS.md +99 -0
- package/skills-library/testing/INTEGRATION_TEST_STRATEGY.md +82 -0
- package/skills-library/testing/RED_GREEN_BUGFIX_GATE.md +203 -0
- package/skills-library/testing/TEST_DATA_MANAGEMENT.md +69 -0
- package/skills-library/testing/VITEST_UNIT_TEST_PATTERNS.md +75 -0
- package/skills-library/testing/playwright-api-security-tests.md +202 -0
- package/skills-library/toolbox/SKILL.md +84 -0
- package/skills-library/toolbox/code-graph-and-web-scraping-mcps.md +237 -0
- package/skills-library/ui-ux-pro-max/ACCESSIBILITY_ESSENTIALS.md +115 -0
- package/skills-library/ui-ux-pro-max/DESIGN_SYSTEM_SCAFFOLDING.md +133 -0
- package/skills-library/ui-ux-pro-max/RESPONSIVE_LAYOUT_PATTERNS.md +119 -0
- package/skills-library/ui-ux-pro-max/SKILL.md +386 -0
- package/skills-library/ui-ux-pro-max/data/charts.csv +26 -0
- package/skills-library/ui-ux-pro-max/data/colors.csv +97 -0
- package/skills-library/ui-ux-pro-max/data/icons.csv +101 -0
- package/skills-library/ui-ux-pro-max/data/landing.csv +31 -0
- package/skills-library/ui-ux-pro-max/data/products.csv +97 -0
- package/skills-library/ui-ux-pro-max/data/react-performance.csv +45 -0
- package/skills-library/ui-ux-pro-max/data/stacks/astro.csv +54 -0
- package/skills-library/ui-ux-pro-max/data/stacks/flutter.csv +53 -0
- package/skills-library/ui-ux-pro-max/data/stacks/html-tailwind.csv +56 -0
- package/skills-library/ui-ux-pro-max/data/stacks/jetpack-compose.csv +53 -0
- package/skills-library/ui-ux-pro-max/data/stacks/nextjs.csv +53 -0
- package/skills-library/ui-ux-pro-max/data/stacks/nuxt-ui.csv +51 -0
- package/skills-library/ui-ux-pro-max/data/stacks/nuxtjs.csv +59 -0
- package/skills-library/ui-ux-pro-max/data/stacks/react-native.csv +52 -0
- package/skills-library/ui-ux-pro-max/data/stacks/react.csv +54 -0
- package/skills-library/ui-ux-pro-max/data/stacks/shadcn.csv +61 -0
- package/skills-library/ui-ux-pro-max/data/stacks/svelte.csv +54 -0
- package/skills-library/ui-ux-pro-max/data/stacks/swiftui.csv +51 -0
- package/skills-library/ui-ux-pro-max/data/stacks/vue.csv +50 -0
- package/skills-library/ui-ux-pro-max/data/styles.csv +68 -0
- package/skills-library/ui-ux-pro-max/data/typography.csv +58 -0
- package/skills-library/ui-ux-pro-max/data/ui-reasoning.csv +101 -0
- package/skills-library/ui-ux-pro-max/data/ux-guidelines.csv +100 -0
- package/skills-library/ui-ux-pro-max/data/web-interface.csv +31 -0
- package/skills-library/wordpress-style-theme-components.md +1526 -0
- package/templates/ASSUMPTIONS.md +1 -1
- package/templates/DECISION_LOG.md +0 -1
- package/templates/phase-prompt.md +1 -1
- package/templates/phoenix-comparison.md +6 -6
- package/templates/skill-api-integration.md +106 -0
- package/templates/skill-architecture-pattern.md +92 -0
- package/templates/skill-debug-pattern.md +98 -0
- package/templates/skill-devops-recipe.md +107 -0
- package/templates/skill-general.md +65 -0
- package/templates/skill-ui-component.md +113 -0
- package/tools/uat-runner.py +179 -0
- package/version.json +7 -3
- package/workflows/handoff-session.md +2 -2
- package/workflows/new-project.md +2 -2
- package/workflows/plan-phase.md +1 -1
- package/.claude-plugin/plugin.json +0 -64
- package/skills-library/_general/methodology/LIVE_BREADCRUMB_PROTOCOL.md +0 -242
- package/skills-library/_general/methodology/llm-judge-memory-crud.md +0 -241
- package/skills-library/methodology/REFLEXION_MEMORY_PATTERN.md +0 -183
- package/skills-library/methodology/RESEARCH_BACKED_WORKFLOW_UPGRADE.md +0 -263
- package/skills-library/methodology/SABBATH_REST_PATTERN.md +0 -267
- package/skills-library/methodology/STONE_AND_SCAFFOLD.md +0 -220
- package/skills-library/specialists/api-architecture/api-designer.md +0 -49
- package/skills-library/specialists/api-architecture/graphql-architect.md +0 -49
- package/skills-library/specialists/api-architecture/mcp-developer.md +0 -51
- package/skills-library/specialists/api-architecture/microservices-architect.md +0 -50
- package/skills-library/specialists/api-architecture/websocket-engineer.md +0 -48
- package/skills-library/specialists/backend/django-expert.md +0 -52
- package/skills-library/specialists/backend/fastapi-expert.md +0 -52
- package/skills-library/specialists/backend/laravel-specialist.md +0 -52
- package/skills-library/specialists/backend/nestjs-expert.md +0 -51
- package/skills-library/specialists/backend/rails-expert.md +0 -53
- package/skills-library/specialists/backend/spring-boot-engineer.md +0 -56
- package/skills-library/specialists/data-ml/fine-tuning-expert.md +0 -48
- package/skills-library/specialists/data-ml/ml-pipeline.md +0 -47
- package/skills-library/specialists/data-ml/pandas-pro.md +0 -47
- package/skills-library/specialists/data-ml/rag-architect.md +0 -51
- package/skills-library/specialists/data-ml/spark-engineer.md +0 -47
- package/skills-library/specialists/frontend/angular-architect.md +0 -52
- package/skills-library/specialists/frontend/flutter-expert.md +0 -51
- package/skills-library/specialists/frontend/nextjs-developer.md +0 -54
- package/skills-library/specialists/frontend/react-native-expert.md +0 -50
- package/skills-library/specialists/frontend/vue-expert.md +0 -51
- package/skills-library/specialists/infrastructure/chaos-engineer.md +0 -74
- package/skills-library/specialists/infrastructure/cloud-architect.md +0 -70
- package/skills-library/specialists/infrastructure/database-optimizer.md +0 -64
- package/skills-library/specialists/infrastructure/devops-engineer.md +0 -70
- package/skills-library/specialists/infrastructure/kubernetes-specialist.md +0 -52
- package/skills-library/specialists/infrastructure/monitoring-expert.md +0 -70
- package/skills-library/specialists/infrastructure/sre-engineer.md +0 -70
- package/skills-library/specialists/infrastructure/terraform-engineer.md +0 -51
- package/skills-library/specialists/languages/cpp-pro.md +0 -74
- package/skills-library/specialists/languages/csharp-developer.md +0 -69
- package/skills-library/specialists/languages/dotnet-core-expert.md +0 -54
- package/skills-library/specialists/languages/golang-pro.md +0 -51
- package/skills-library/specialists/languages/java-architect.md +0 -49
- package/skills-library/specialists/languages/javascript-pro.md +0 -68
- package/skills-library/specialists/languages/kotlin-specialist.md +0 -68
- package/skills-library/specialists/languages/php-pro.md +0 -49
- package/skills-library/specialists/languages/python-pro.md +0 -52
- package/skills-library/specialists/languages/react-expert.md +0 -51
- package/skills-library/specialists/languages/rust-engineer.md +0 -50
- package/skills-library/specialists/languages/sql-pro.md +0 -56
- package/skills-library/specialists/languages/swift-expert.md +0 -69
- package/skills-library/specialists/languages/typescript-pro.md +0 -51
- package/skills-library/specialists/platform/atlassian-mcp.md +0 -52
- package/skills-library/specialists/platform/embedded-systems.md +0 -53
- package/skills-library/specialists/platform/game-developer.md +0 -53
- package/skills-library/specialists/platform/salesforce-developer.md +0 -53
- package/skills-library/specialists/platform/shopify-expert.md +0 -49
- package/skills-library/specialists/platform/wordpress-pro.md +0 -49
- package/skills-library/specialists/quality/code-documenter.md +0 -51
- package/skills-library/specialists/quality/code-reviewer.md +0 -67
- package/skills-library/specialists/quality/debugging-wizard.md +0 -51
- package/skills-library/specialists/quality/fullstack-guardian.md +0 -51
- package/skills-library/specialists/quality/legacy-modernizer.md +0 -50
- package/skills-library/specialists/quality/playwright-expert.md +0 -65
- package/skills-library/specialists/quality/spec-miner.md +0 -56
- package/skills-library/specialists/quality/test-master.md +0 -65
- package/skills-library/specialists/security/secure-code-guardian.md +0 -55
- package/skills-library/specialists/security/security-reviewer.md +0 -53
- package/skills-library/specialists/workflow/architecture-designer.md +0 -53
- package/skills-library/specialists/workflow/cli-developer.md +0 -70
- package/skills-library/specialists/workflow/feature-forge.md +0 -65
- package/skills-library/specialists/workflow/prompt-engineer.md +0 -54
- package/skills-library/specialists/workflow/the-fool.md +0 -62
- /package/skills-library/{performance → _general/performance}/cache-augmented-generation.md +0 -0
- /package/skills-library/{debugging → parallel-debug}/FAILURE_TAXONOMY_CLASSIFICATION.md +0 -0
- /package/skills-library/{debugging → parallel-debug}/THREE_AGENT_HYPOTHESIS_DEBUGGING.md +0 -0
|
@@ -0,0 +1,1355 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: podcast-audio-composition
|
|
3
|
+
category: creative-multimedia
|
|
4
|
+
version: 1.0.0
|
|
5
|
+
contributed: 2026-03-10
|
|
6
|
+
contributor: dominion-flow-research
|
|
7
|
+
last_updated: 2026-03-10
|
|
8
|
+
tags: [podcast, audio, ffmpeg, mixing, composition, multi-track, intro-outro, background-music]
|
|
9
|
+
difficulty: medium
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# Podcast Audio Composition
|
|
13
|
+
**Related skills:** [ffmpeg-command-generator.md](ffmpeg-command-generator.md), [audio-enhancement-pipeline.md](audio-enhancement-pipeline.md)
|
|
14
|
+
|
|
15
|
+
## Description
|
|
16
|
+
|
|
17
|
+
Compose multi-track podcast audio from individual speech segments, background music, intro/outro jingles, and sound effects — all using FFmpeg and Node.js. This skill covers the full production workflow: assembling TTS speech segments in script order, mixing music beds underneath dialogue, adding intro/outro with crossfades, applying per-speaker EQ and spatial separation, and exporting with proper loudness normalization and metadata.
|
|
18
|
+
|
|
19
|
+
## When to Use
|
|
20
|
+
|
|
21
|
+
- Building AI-generated podcasts from TTS segments (Google Cloud TTS, Anthropic Claude voice, Gemini TTS)
|
|
22
|
+
- Composing multi-speaker audio from individual voice recordings
|
|
23
|
+
- Adding background music, intro jingles, and sound effects to speech
|
|
24
|
+
- Automating podcast post-production in a Node.js pipeline
|
|
25
|
+
- Creating educational narration with chapter breaks and ambient music
|
|
26
|
+
- Producing sermon recap audio with multiple speakers
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## 1. Multi-Speaker Audio Assembly
|
|
31
|
+
|
|
32
|
+
### Concept
|
|
33
|
+
|
|
34
|
+
Each speaker's lines are separate audio files (from TTS or recordings). A script JSON defines the assembly order. The composer concatenates them with natural pauses and optional crossfades.
|
|
35
|
+
|
|
36
|
+
### Script JSON Format
|
|
37
|
+
|
|
38
|
+
```json
|
|
39
|
+
{
|
|
40
|
+
"title": "Episode 42: Faith and Technology",
|
|
41
|
+
"speakers": {
|
|
42
|
+
"host": { "name": "Pastor James", "voice": "en-US-Neural2-D", "role": "host" },
|
|
43
|
+
"guest": { "name": "Dr. Sarah Chen", "voice": "en-US-Neural2-F", "role": "guest" }
|
|
44
|
+
},
|
|
45
|
+
"segments": [
|
|
46
|
+
{ "speaker": "host", "file": "segments/001_host_intro.wav", "text": "Welcome to..." },
|
|
47
|
+
{ "speaker": "guest", "file": "segments/002_guest_response.wav", "text": "Thanks for having me..." },
|
|
48
|
+
{ "speaker": "host", "file": "segments/003_host_question.wav", "text": "So tell us about..." },
|
|
49
|
+
{ "speaker": "guest", "file": "segments/004_guest_answer.wav", "text": "Well, the key insight is..." },
|
|
50
|
+
{ "speaker": "host", "file": "segments/005_host_followup.wav", "text": "That's fascinating..." },
|
|
51
|
+
{ "speaker": "guest", "file": "segments/006_guest_elaboration.wav", "text": "Exactly, and when you consider..." },
|
|
52
|
+
{ "speaker": "host", "file": "segments/007_host_closing.wav", "text": "Thank you so much..." }
|
|
53
|
+
],
|
|
54
|
+
"music": {
|
|
55
|
+
"intro": "assets/intro_jingle.mp3",
|
|
56
|
+
"outro": "assets/outro_jingle.mp3",
|
|
57
|
+
"bed": "assets/ambient_music_bed.mp3"
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### Pause Strategy
|
|
63
|
+
|
|
64
|
+
Natural conversation has variable pauses. Robotic podcasts use identical gaps.
|
|
65
|
+
|
|
66
|
+
| Transition | Pause Duration | Rationale |
|
|
67
|
+
|-----------|---------------|-----------|
|
|
68
|
+
| Same speaker continues | 200ms | Brief breath pause |
|
|
69
|
+
| Speaker change (agreement) | 300ms | Natural handoff |
|
|
70
|
+
| Speaker change (new topic) | 500ms | Topic transition |
|
|
71
|
+
| After a question | 400ms | Thinking pause |
|
|
72
|
+
| Chapter break | 1000ms | Clear section separation |
|
|
73
|
+
| Before closing remarks | 800ms | Signals wind-down |
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
## 2. FFmpeg Filter Graphs for Podcast Production
|
|
78
|
+
|
|
79
|
+
### a) Generate Silence Padding
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
# Create silence files at 44100 Hz mono (match your TTS sample rate)
|
|
83
|
+
ffmpeg -f lavfi -i anullsrc=r=44100:cl=mono -t 0.2 silence_200ms.wav
|
|
84
|
+
ffmpeg -f lavfi -i anullsrc=r=44100:cl=mono -t 0.3 silence_300ms.wav
|
|
85
|
+
ffmpeg -f lavfi -i anullsrc=r=44100:cl=mono -t 0.5 silence_500ms.wav
|
|
86
|
+
ffmpeg -f lavfi -i anullsrc=r=44100:cl=mono -t 1.0 silence_1000ms.wav
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### b) Concatenate Speech Segments with Silence Gaps
|
|
90
|
+
|
|
91
|
+
Create a `segments.txt` file listing each segment and silence in order:
|
|
92
|
+
|
|
93
|
+
```
|
|
94
|
+
file 'segments/001_host_intro.wav'
|
|
95
|
+
file 'silence_300ms.wav'
|
|
96
|
+
file 'segments/002_guest_response.wav'
|
|
97
|
+
file 'silence_300ms.wav'
|
|
98
|
+
file 'segments/003_host_question.wav'
|
|
99
|
+
file 'silence_400ms.wav'
|
|
100
|
+
file 'segments/004_guest_answer.wav'
|
|
101
|
+
file 'silence_300ms.wav'
|
|
102
|
+
file 'segments/005_host_followup.wav'
|
|
103
|
+
file 'silence_300ms.wav'
|
|
104
|
+
file 'segments/006_guest_elaboration.wav'
|
|
105
|
+
file 'silence_500ms.wav'
|
|
106
|
+
file 'segments/007_host_closing.wav'
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
# Concatenate all segments + silences into one continuous track
|
|
111
|
+
ffmpeg -f concat -safe 0 -i segments.txt -c copy concatenated.wav
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
**Important:** All input files must have the same sample rate, channel count, and codec. If they differ, re-encode first:
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
# Normalize all segments to 44100 Hz mono PCM before concatenation
|
|
118
|
+
for f in segments/*.wav; do
|
|
119
|
+
ffmpeg -i "$f" -ar 44100 -ac 1 -c:a pcm_s16le "normalized_$(basename $f)"
|
|
120
|
+
done
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### c) Crossfade Between Segments (Smoother Transitions)
|
|
124
|
+
|
|
125
|
+
For a more polished sound, crossfade instead of hard-cut with silence:
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
# Crossfade two segments (0.3s overlap, triangular curve)
|
|
129
|
+
ffmpeg -i segment_01.wav -i segment_02.wav \
|
|
130
|
+
-filter_complex "[0:a][1:a]acrossfade=d=0.3:c1=tri:c2=tri" \
|
|
131
|
+
crossfaded.wav
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
For chaining multiple crossfades (3+ segments):
|
|
135
|
+
|
|
136
|
+
```bash
|
|
137
|
+
# Chain 4 segments with 0.3s crossfades
|
|
138
|
+
ffmpeg -i seg1.wav -i seg2.wav -i seg3.wav -i seg4.wav \
|
|
139
|
+
-filter_complex \
|
|
140
|
+
"[0:a][1:a]acrossfade=d=0.3:c1=tri:c2=tri[ab]; \
|
|
141
|
+
[ab][2:a]acrossfade=d=0.3:c1=tri:c2=tri[abc]; \
|
|
142
|
+
[abc][3:a]acrossfade=d=0.3:c1=tri:c2=tri" \
|
|
143
|
+
chained.wav
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
**Crossfade curve options:**
|
|
147
|
+
- `tri` — triangular (linear fade, natural for speech)
|
|
148
|
+
- `qsin` — quarter sine (smooth, slightly musical)
|
|
149
|
+
- `esin` — exponential sine (very smooth, best for music transitions)
|
|
150
|
+
- `log` — logarithmic (quick fade, punchy)
|
|
151
|
+
|
|
152
|
+
### d) Mix Background Music Under Speech
|
|
153
|
+
|
|
154
|
+
```bash
|
|
155
|
+
# Simple mix: music at 15% volume underneath speech
|
|
156
|
+
ffmpeg -i speech.wav -i music.wav \
|
|
157
|
+
-filter_complex "[1:a]volume=0.15[music];[0:a][music]amix=inputs=2:duration=first" \
|
|
158
|
+
mixed.mp3
|
|
159
|
+
|
|
160
|
+
# Music fades in over 3s, plays at 12% volume, fades out over 3s
|
|
161
|
+
ffmpeg -i speech.wav -i music.wav \
|
|
162
|
+
-filter_complex \
|
|
163
|
+
"[1:a]volume=0.12,afade=t=in:d=3,afade=t=out:st=SPEECH_DURATION_MINUS_3:d=3[music]; \
|
|
164
|
+
[0:a][music]amix=inputs=2:duration=first" \
|
|
165
|
+
mixed_with_fades.mp3
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
Replace `SPEECH_DURATION_MINUS_3` with the actual speech duration minus 3 seconds. Use `ffprobe` to measure:
|
|
169
|
+
|
|
170
|
+
```bash
|
|
171
|
+
# Get audio duration in seconds
|
|
172
|
+
ffprobe -i speech.wav -show_entries format=duration -v quiet -of csv="p=0"
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
### e) Sidechain Compression (Duck Music Under Speech)
|
|
176
|
+
|
|
177
|
+
More professional than static volume — music automatically ducks when speech is present:
|
|
178
|
+
|
|
179
|
+
```bash
|
|
180
|
+
# Music ducks under speech using sidechaincompress
|
|
181
|
+
ffmpeg -i speech.wav -i music.wav \
|
|
182
|
+
-filter_complex \
|
|
183
|
+
"[1:a]volume=0.25[music]; \
|
|
184
|
+
[music][0:a]sidechaincompress=threshold=0.03:ratio=5:attack=200:release=1000[ducked]; \
|
|
185
|
+
[0:a][ducked]amix=inputs=2:duration=first" \
|
|
186
|
+
ducked_output.mp3
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
**Parameters:**
|
|
190
|
+
- `threshold=0.03` — duck when speech exceeds this level (low = sensitive)
|
|
191
|
+
- `ratio=5` — how much to reduce music (5:1 compression)
|
|
192
|
+
- `attack=200` — how fast music ducks (ms)
|
|
193
|
+
- `release=1000` — how fast music returns after speech stops (ms)
|
|
194
|
+
|
|
195
|
+
### f) Add Intro/Outro with Crossfade
|
|
196
|
+
|
|
197
|
+
```bash
|
|
198
|
+
# Crossfade intro (5s) into main content, then main into outro (5s)
|
|
199
|
+
ffmpeg -i intro.mp3 -i main_content.mp3 -i outro.mp3 \
|
|
200
|
+
-filter_complex \
|
|
201
|
+
"[0:a][1:a]acrossfade=d=2:c1=tri:c2=tri[mid]; \
|
|
202
|
+
[mid][2:a]acrossfade=d=2:c1=tri:c2=tri" \
|
|
203
|
+
final_episode.mp3
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
For a hard-cut intro with fade-in on main content:
|
|
207
|
+
|
|
208
|
+
```bash
|
|
209
|
+
# Intro plays fully, main content fades in, outro fades in at end
|
|
210
|
+
ffmpeg -i intro.mp3 -i main.mp3 -i outro.mp3 \
|
|
211
|
+
-filter_complex \
|
|
212
|
+
"[1:a]afade=t=in:d=1[main_faded]; \
|
|
213
|
+
[0:a][main_faded]concat=n=2:v=0:a=1[with_intro]; \
|
|
214
|
+
[2:a]afade=t=in:d=1[outro_faded]; \
|
|
215
|
+
[with_intro][outro_faded]concat=n=2:v=0:a=1" \
|
|
216
|
+
final.mp3
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
### g) Loudness Normalization (EBU R128 — Podcast Standard)
|
|
220
|
+
|
|
221
|
+
Two-pass normalization for broadcast-quality consistency:
|
|
222
|
+
|
|
223
|
+
```bash
|
|
224
|
+
# Pass 1: Measure current loudness (capture JSON output)
|
|
225
|
+
ffmpeg -i input.wav -af loudnorm=I=-16:TP=-1.5:LRA=11:print_format=json -f null NUL 2>&1
|
|
226
|
+
|
|
227
|
+
# Pass 2: Apply measured values for precise normalization
|
|
228
|
+
# Replace the measured_* values with output from Pass 1
|
|
229
|
+
ffmpeg -i input.wav \
|
|
230
|
+
-af loudnorm=I=-16:TP=-1.5:LRA=11:measured_I=-23.5:measured_TP=-4.2:measured_LRA=14.1:measured_thresh=-34.8:linear=true \
|
|
231
|
+
-c:a libmp3lame -b:a 192k -ar 44100 \
|
|
232
|
+
normalized.mp3
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
| Parameter | Value | Purpose |
|
|
236
|
+
|-----------|-------|---------|
|
|
237
|
+
| `I=-16` | -16 LUFS | Integrated loudness target (podcast standard) |
|
|
238
|
+
| `TP=-1.5` | -1.5 dBTP | True peak ceiling (prevents clipping on decode) |
|
|
239
|
+
| `LRA=11` | 11 LU | Loudness range (dynamic range window) |
|
|
240
|
+
| `linear=true` | — | Use linear normalization (preserves dynamics better) |
|
|
241
|
+
|
|
242
|
+
Single-pass shortcut (less precise but simpler):
|
|
243
|
+
|
|
244
|
+
```bash
|
|
245
|
+
ffmpeg -i input.wav -af loudnorm=I=-16:TP=-1.5:LRA=11 -c:a libmp3lame -b:a 192k normalized.mp3
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
---
|
|
249
|
+
|
|
250
|
+
## 3. Node.js fluent-ffmpeg Wrapper
|
|
251
|
+
|
|
252
|
+
### Type Definitions
|
|
253
|
+
|
|
254
|
+
```typescript
|
|
255
|
+
import ffmpeg from 'fluent-ffmpeg';
|
|
256
|
+
import { existsSync, writeFileSync, unlinkSync, mkdirSync } from 'fs';
|
|
257
|
+
import { join, basename, dirname } from 'path';
|
|
258
|
+
import { execFileSync } from 'child_process';
|
|
259
|
+
|
|
260
|
+
// --- Type Definitions ---
|
|
261
|
+
|
|
262
|
+
interface AudioSegment {
|
|
263
|
+
speaker: string;
|
|
264
|
+
file: string;
|
|
265
|
+
text?: string;
|
|
266
|
+
pauseAfterMs?: number; // Override default pause (200-500ms)
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
interface SpeakerProfile {
|
|
270
|
+
name: string;
|
|
271
|
+
voice: string;
|
|
272
|
+
role: 'host' | 'guest' | 'narrator';
|
|
273
|
+
eqPreset?: 'deep' | 'bright' | 'neutral';
|
|
274
|
+
pan?: number; // -1.0 (full left) to 1.0 (full right)
|
|
275
|
+
volumeAdjust?: number; // dB adjustment (-3 to +3)
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
interface PodcastMetadata {
|
|
279
|
+
title: string;
|
|
280
|
+
artist: string;
|
|
281
|
+
album?: string;
|
|
282
|
+
date?: string;
|
|
283
|
+
genre?: string;
|
|
284
|
+
comment?: string;
|
|
285
|
+
trackNumber?: number;
|
|
286
|
+
coverArt?: string; // Path to cover image
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
interface CompositionConfig {
|
|
290
|
+
segments: AudioSegment[];
|
|
291
|
+
speakers: Record<string, SpeakerProfile>;
|
|
292
|
+
music?: {
|
|
293
|
+
intro?: string;
|
|
294
|
+
outro?: string;
|
|
295
|
+
bed?: string;
|
|
296
|
+
bedVolume?: number; // 0.0 - 1.0, default 0.12
|
|
297
|
+
};
|
|
298
|
+
crossfadeDuration?: number; // seconds, default 2
|
|
299
|
+
defaultPauseMs?: number; // default 300
|
|
300
|
+
outputFormat?: 'mp3' | 'wav' | 'ogg';
|
|
301
|
+
outputPath: string;
|
|
302
|
+
metadata?: PodcastMetadata;
|
|
303
|
+
tempDir?: string;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// --- Helper Functions ---
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Get audio duration using ffprobe.
|
|
310
|
+
* Uses execFileSync (no shell) to avoid command injection.
|
|
311
|
+
*/
|
|
312
|
+
function getAudioDuration(filePath: string): number {
|
|
313
|
+
const output = execFileSync('ffprobe', [
|
|
314
|
+
'-i', filePath,
|
|
315
|
+
'-show_entries', 'format=duration',
|
|
316
|
+
'-v', 'quiet',
|
|
317
|
+
'-of', 'csv=p=0'
|
|
318
|
+
], { encoding: 'utf-8' });
|
|
319
|
+
return parseFloat(output.trim());
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Run an FFmpeg command safely using execFileSync (no shell).
|
|
324
|
+
* All arguments are passed as an array to prevent injection.
|
|
325
|
+
*/
|
|
326
|
+
function runFfmpegSync(args: string[]): string {
|
|
327
|
+
return execFileSync('ffmpeg', args, { encoding: 'utf-8', stdio: 'pipe' });
|
|
328
|
+
}
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
### PodcastComposer Class
|
|
332
|
+
|
|
333
|
+
```typescript
|
|
334
|
+
class PodcastComposer {
|
|
335
|
+
private tempDir: string;
|
|
336
|
+
private tempFiles: string[] = [];
|
|
337
|
+
|
|
338
|
+
constructor(tempDir: string = './temp_podcast') {
|
|
339
|
+
this.tempDir = tempDir;
|
|
340
|
+
mkdirSync(this.tempDir, { recursive: true });
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Generate a silence file of specified duration.
|
|
345
|
+
*/
|
|
346
|
+
private createSilence(durationMs: number): string {
|
|
347
|
+
const outPath = join(this.tempDir, `silence_${durationMs}ms.wav`);
|
|
348
|
+
if (!existsSync(outPath)) {
|
|
349
|
+
runFfmpegSync([
|
|
350
|
+
'-y', '-f', 'lavfi',
|
|
351
|
+
'-i', `anullsrc=r=44100:cl=mono`,
|
|
352
|
+
'-t', String(durationMs / 1000),
|
|
353
|
+
outPath
|
|
354
|
+
]);
|
|
355
|
+
this.tempFiles.push(outPath);
|
|
356
|
+
}
|
|
357
|
+
return outPath;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Normalize a segment to consistent sample rate and channels.
|
|
362
|
+
*/
|
|
363
|
+
private normalizeSegment(inputPath: string, sampleRate: number = 44100): string {
|
|
364
|
+
const outPath = join(this.tempDir, `norm_${basename(inputPath)}`);
|
|
365
|
+
runFfmpegSync([
|
|
366
|
+
'-y', '-i', inputPath,
|
|
367
|
+
'-ar', String(sampleRate),
|
|
368
|
+
'-ac', '1',
|
|
369
|
+
'-c:a', 'pcm_s16le',
|
|
370
|
+
outPath
|
|
371
|
+
]);
|
|
372
|
+
this.tempFiles.push(outPath);
|
|
373
|
+
return outPath;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Apply speaker-specific EQ profile to a segment.
|
|
378
|
+
*/
|
|
379
|
+
private applySpeakerEQ(inputPath: string, profile: SpeakerProfile): string {
|
|
380
|
+
const outPath = join(this.tempDir, `eq_${basename(inputPath)}`);
|
|
381
|
+
const filters: string[] = [];
|
|
382
|
+
|
|
383
|
+
// EQ presets for voice differentiation
|
|
384
|
+
switch (profile.eqPreset) {
|
|
385
|
+
case 'deep':
|
|
386
|
+
// Host: warmer, fuller low-mid presence
|
|
387
|
+
filters.push('equalizer=f=200:t=q:w=1.0:g=2');
|
|
388
|
+
filters.push('equalizer=f=3000:t=q:w=1.5:g=1');
|
|
389
|
+
filters.push('highpass=f=60');
|
|
390
|
+
break;
|
|
391
|
+
case 'bright':
|
|
392
|
+
// Guest: clearer, more articulate high-mid
|
|
393
|
+
filters.push('equalizer=f=3500:t=q:w=1.5:g=3');
|
|
394
|
+
filters.push('equalizer=f=6000:t=q:w=2.0:g=1.5');
|
|
395
|
+
filters.push('highpass=f=100');
|
|
396
|
+
break;
|
|
397
|
+
case 'neutral':
|
|
398
|
+
default:
|
|
399
|
+
// Clean pass — minimal processing
|
|
400
|
+
filters.push('highpass=f=80');
|
|
401
|
+
break;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// Volume adjustment
|
|
405
|
+
if (profile.volumeAdjust) {
|
|
406
|
+
filters.push(`volume=${profile.volumeAdjust}dB`);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// Stereo panning (convert mono to stereo with pan position)
|
|
410
|
+
if (profile.pan !== undefined && profile.pan !== 0) {
|
|
411
|
+
const leftGain = Math.cos((profile.pan + 1) * Math.PI / 4);
|
|
412
|
+
const rightGain = Math.sin((profile.pan + 1) * Math.PI / 4);
|
|
413
|
+
filters.push(`pan=stereo|c0=${leftGain.toFixed(3)}*c0|c1=${rightGain.toFixed(3)}*c0`);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
if (filters.length === 0) return inputPath;
|
|
417
|
+
|
|
418
|
+
const filterChain = filters.join(',');
|
|
419
|
+
runFfmpegSync(['-y', '-i', inputPath, '-af', filterChain, outPath]);
|
|
420
|
+
this.tempFiles.push(outPath);
|
|
421
|
+
return outPath;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* Assemble speech segments with silence gaps between them.
|
|
426
|
+
* Normalizes all segments, applies speaker EQ, then concatenates.
|
|
427
|
+
*/
|
|
428
|
+
async assembleSpeechSegments(
|
|
429
|
+
segments: AudioSegment[],
|
|
430
|
+
speakers: Record<string, SpeakerProfile> = {},
|
|
431
|
+
defaultPauseMs: number = 300
|
|
432
|
+
): Promise<string> {
|
|
433
|
+
const outputPath = join(this.tempDir, 'assembled_speech.wav');
|
|
434
|
+
const concatListPath = join(this.tempDir, 'concat_list.txt');
|
|
435
|
+
const lines: string[] = [];
|
|
436
|
+
|
|
437
|
+
for (let i = 0; i < segments.length; i++) {
|
|
438
|
+
const seg = segments[i];
|
|
439
|
+
|
|
440
|
+
if (!existsSync(seg.file)) {
|
|
441
|
+
throw new Error(`Segment file not found: ${seg.file}`);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// Normalize to consistent format
|
|
445
|
+
let processed = this.normalizeSegment(seg.file);
|
|
446
|
+
|
|
447
|
+
// Apply speaker-specific EQ if profile exists
|
|
448
|
+
const profile = speakers[seg.speaker];
|
|
449
|
+
if (profile) {
|
|
450
|
+
processed = this.applySpeakerEQ(processed, profile);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
lines.push(`file '${processed.replace(/\\/g, '/')}'`);
|
|
454
|
+
|
|
455
|
+
// Add silence gap after each segment (except the last)
|
|
456
|
+
if (i < segments.length - 1) {
|
|
457
|
+
const pauseMs = seg.pauseAfterMs || defaultPauseMs;
|
|
458
|
+
const silencePath = this.createSilence(pauseMs);
|
|
459
|
+
lines.push(`file '${silencePath.replace(/\\/g, '/')}'`);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
writeFileSync(concatListPath, lines.join('\n'), 'utf-8');
|
|
464
|
+
|
|
465
|
+
runFfmpegSync([
|
|
466
|
+
'-y', '-f', 'concat', '-safe', '0',
|
|
467
|
+
'-i', concatListPath,
|
|
468
|
+
'-c:a', 'pcm_s16le',
|
|
469
|
+
outputPath
|
|
470
|
+
]);
|
|
471
|
+
|
|
472
|
+
this.tempFiles.push(outputPath, concatListPath);
|
|
473
|
+
console.log(`[podcast-composer] Assembled ${segments.length} segments`);
|
|
474
|
+
return outputPath;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/**
|
|
478
|
+
* Mix background music underneath speech audio.
|
|
479
|
+
* Music volume is reduced and fades in/out gracefully.
|
|
480
|
+
*/
|
|
481
|
+
async addBackgroundMusic(
|
|
482
|
+
speechPath: string,
|
|
483
|
+
musicPath: string,
|
|
484
|
+
musicVolume: number = 0.12
|
|
485
|
+
): Promise<string> {
|
|
486
|
+
const outputPath = join(this.tempDir, 'speech_with_music.wav');
|
|
487
|
+
const speechDuration = getAudioDuration(speechPath);
|
|
488
|
+
const fadeOutStart = Math.max(0, speechDuration - 3);
|
|
489
|
+
|
|
490
|
+
const filterGraph =
|
|
491
|
+
`[1:a]volume=${musicVolume},afade=t=in:d=3,` +
|
|
492
|
+
`afade=t=out:st=${fadeOutStart.toFixed(2)}:d=3[music];` +
|
|
493
|
+
`[0:a][music]amix=inputs=2:duration=first`;
|
|
494
|
+
|
|
495
|
+
runFfmpegSync([
|
|
496
|
+
'-y', '-i', speechPath, '-i', musicPath,
|
|
497
|
+
'-filter_complex', filterGraph,
|
|
498
|
+
outputPath
|
|
499
|
+
]);
|
|
500
|
+
|
|
501
|
+
this.tempFiles.push(outputPath);
|
|
502
|
+
console.log(`[podcast-composer] Added background music (vol=${musicVolume})`);
|
|
503
|
+
return outputPath;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
/**
|
|
507
|
+
* Add intro and/or outro with crossfade transitions.
|
|
508
|
+
*/
|
|
509
|
+
async addIntroOutro(
|
|
510
|
+
mainPath: string,
|
|
511
|
+
intro?: string,
|
|
512
|
+
outro?: string,
|
|
513
|
+
crossfadeSec: number = 2
|
|
514
|
+
): Promise<string> {
|
|
515
|
+
let currentPath = mainPath;
|
|
516
|
+
|
|
517
|
+
// Add intro with crossfade
|
|
518
|
+
if (intro && existsSync(intro)) {
|
|
519
|
+
const withIntro = join(this.tempDir, 'with_intro.wav');
|
|
520
|
+
runFfmpegSync([
|
|
521
|
+
'-y', '-i', intro, '-i', currentPath,
|
|
522
|
+
'-filter_complex', `[0:a][1:a]acrossfade=d=${crossfadeSec}:c1=tri:c2=tri`,
|
|
523
|
+
withIntro
|
|
524
|
+
]);
|
|
525
|
+
this.tempFiles.push(withIntro);
|
|
526
|
+
currentPath = withIntro;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// Add outro with crossfade
|
|
530
|
+
if (outro && existsSync(outro)) {
|
|
531
|
+
const withOutro = join(this.tempDir, 'with_intro_outro.wav');
|
|
532
|
+
runFfmpegSync([
|
|
533
|
+
'-y', '-i', currentPath, '-i', outro,
|
|
534
|
+
'-filter_complex', `[0:a][1:a]acrossfade=d=${crossfadeSec}:c1=tri:c2=tri`,
|
|
535
|
+
withOutro
|
|
536
|
+
]);
|
|
537
|
+
this.tempFiles.push(withOutro);
|
|
538
|
+
currentPath = withOutro;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
console.log(`[podcast-composer] Added intro/outro`);
|
|
542
|
+
return currentPath;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
/**
|
|
546
|
+
* Two-pass EBU R128 loudness normalization.
|
|
547
|
+
* Targets -16 LUFS (podcast standard).
|
|
548
|
+
*/
|
|
549
|
+
async normalizeAudio(inputPath: string): Promise<string> {
|
|
550
|
+
const outputPath = join(this.tempDir, 'normalized.wav');
|
|
551
|
+
|
|
552
|
+
// Pass 1: Measure current loudness
|
|
553
|
+
// Note: FFmpeg writes loudnorm JSON to stderr, so we capture it
|
|
554
|
+
let measureOutput: string;
|
|
555
|
+
try {
|
|
556
|
+
execFileSync('ffmpeg', [
|
|
557
|
+
'-i', inputPath,
|
|
558
|
+
'-af', 'loudnorm=I=-16:TP=-1.5:LRA=11:print_format=json',
|
|
559
|
+
'-f', 'null', 'NUL'
|
|
560
|
+
], { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] });
|
|
561
|
+
measureOutput = '';
|
|
562
|
+
} catch (err: any) {
|
|
563
|
+
// FFmpeg exits non-zero for -f null but stderr has our data
|
|
564
|
+
measureOutput = err.stderr || '';
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
// Parse measured values from JSON output
|
|
568
|
+
const jsonMatch = measureOutput.match(/\{[\s\S]*?"input_i"[\s\S]*?\}/);
|
|
569
|
+
if (!jsonMatch) {
|
|
570
|
+
throw new Error('Failed to parse loudness measurement from FFmpeg output');
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
const measured = JSON.parse(jsonMatch[0]);
|
|
574
|
+
const { input_i, input_tp, input_lra, input_thresh } = measured;
|
|
575
|
+
|
|
576
|
+
// Pass 2: Apply precise normalization with measured values
|
|
577
|
+
runFfmpegSync([
|
|
578
|
+
'-y', '-i', inputPath,
|
|
579
|
+
'-af', `loudnorm=I=-16:TP=-1.5:LRA=11:measured_I=${input_i}:measured_TP=${input_tp}:measured_LRA=${input_lra}:measured_thresh=${input_thresh}:linear=true`,
|
|
580
|
+
outputPath
|
|
581
|
+
]);
|
|
582
|
+
|
|
583
|
+
this.tempFiles.push(outputPath);
|
|
584
|
+
console.log(`[podcast-composer] Normalized to -16 LUFS (was ${input_i} LUFS)`);
|
|
585
|
+
return outputPath;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
/**
|
|
589
|
+
* Export final podcast with format conversion and metadata tagging.
|
|
590
|
+
*/
|
|
591
|
+
async exportFinal(
|
|
592
|
+
inputPath: string,
|
|
593
|
+
format: 'mp3' | 'wav' | 'ogg' = 'mp3',
|
|
594
|
+
metadata?: PodcastMetadata
|
|
595
|
+
): Promise<string> {
|
|
596
|
+
const ext = format === 'ogg' ? 'ogg' : format;
|
|
597
|
+
const outputPath = join(dirname(this.tempDir), `final_podcast.${ext}`);
|
|
598
|
+
|
|
599
|
+
const codecMap: Record<string, { codec: string; bitrate: string }> = {
|
|
600
|
+
mp3: { codec: 'libmp3lame', bitrate: '192k' },
|
|
601
|
+
wav: { codec: 'pcm_s16le', bitrate: '' },
|
|
602
|
+
ogg: { codec: 'libvorbis', bitrate: '192k' },
|
|
603
|
+
};
|
|
604
|
+
|
|
605
|
+
const { codec, bitrate } = codecMap[format];
|
|
606
|
+
|
|
607
|
+
// Build FFmpeg args array
|
|
608
|
+
const args: string[] = ['-y', '-i', inputPath, '-c:a', codec];
|
|
609
|
+
|
|
610
|
+
if (bitrate) {
|
|
611
|
+
args.push('-b:a', bitrate);
|
|
612
|
+
}
|
|
613
|
+
args.push('-ar', '44100');
|
|
614
|
+
|
|
615
|
+
// Add metadata flags
|
|
616
|
+
if (metadata) {
|
|
617
|
+
if (metadata.title) args.push('-metadata', `title=${metadata.title}`);
|
|
618
|
+
if (metadata.artist) args.push('-metadata', `artist=${metadata.artist}`);
|
|
619
|
+
if (metadata.album) args.push('-metadata', `album=${metadata.album}`);
|
|
620
|
+
if (metadata.date) args.push('-metadata', `date=${metadata.date}`);
|
|
621
|
+
if (metadata.genre) args.push('-metadata', `genre=${metadata.genre}`);
|
|
622
|
+
if (metadata.comment) args.push('-metadata', `comment=${metadata.comment}`);
|
|
623
|
+
if (metadata.trackNumber) args.push('-metadata', `track=${metadata.trackNumber}`);
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
args.push(outputPath);
|
|
627
|
+
runFfmpegSync(args);
|
|
628
|
+
|
|
629
|
+
// Embed cover art for MP3 (if provided)
|
|
630
|
+
if (format === 'mp3' && metadata?.coverArt && existsSync(metadata.coverArt)) {
|
|
631
|
+
const withCover = outputPath.replace('.mp3', '_cover.mp3');
|
|
632
|
+
runFfmpegSync([
|
|
633
|
+
'-y', '-i', outputPath, '-i', metadata.coverArt,
|
|
634
|
+
'-map', '0:a', '-map', '1:0',
|
|
635
|
+
'-c', 'copy', '-id3v2_version', '3',
|
|
636
|
+
'-metadata:s:v', 'title=Album cover',
|
|
637
|
+
'-metadata:s:v', 'comment=Cover (front)',
|
|
638
|
+
withCover
|
|
639
|
+
]);
|
|
640
|
+
unlinkSync(outputPath);
|
|
641
|
+
require('fs').renameSync(withCover, outputPath);
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
console.log(`[podcast-composer] Exported final podcast`);
|
|
645
|
+
return outputPath;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
/**
|
|
649
|
+
* Full composition pipeline: assemble -> music -> intro/outro -> normalize -> export.
|
|
650
|
+
* This is the main entry point for end-to-end podcast production.
|
|
651
|
+
*/
|
|
652
|
+
async compose(config: CompositionConfig): Promise<string> {
|
|
653
|
+
console.log(`[podcast-composer] Starting composition: ${config.metadata?.title || 'Untitled'}`);
|
|
654
|
+
const startTime = Date.now();
|
|
655
|
+
|
|
656
|
+
// Step 1: Assemble speech segments with pauses
|
|
657
|
+
let audioPath = await this.assembleSpeechSegments(
|
|
658
|
+
config.segments,
|
|
659
|
+
config.speakers,
|
|
660
|
+
config.defaultPauseMs
|
|
661
|
+
);
|
|
662
|
+
|
|
663
|
+
// Step 2: Mix background music (if provided)
|
|
664
|
+
if (config.music?.bed && existsSync(config.music.bed)) {
|
|
665
|
+
audioPath = await this.addBackgroundMusic(
|
|
666
|
+
audioPath,
|
|
667
|
+
config.music.bed,
|
|
668
|
+
config.music.bedVolume || 0.12
|
|
669
|
+
);
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
// Step 3: Add intro/outro (if provided)
|
|
673
|
+
if (config.music?.intro || config.music?.outro) {
|
|
674
|
+
audioPath = await this.addIntroOutro(
|
|
675
|
+
audioPath,
|
|
676
|
+
config.music.intro,
|
|
677
|
+
config.music.outro,
|
|
678
|
+
config.crossfadeDuration || 2
|
|
679
|
+
);
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
// Step 4: Loudness normalization (EBU R128, -16 LUFS)
|
|
683
|
+
audioPath = await this.normalizeAudio(audioPath);
|
|
684
|
+
|
|
685
|
+
// Step 5: Export to final format with metadata
|
|
686
|
+
const finalPath = await this.exportFinal(
|
|
687
|
+
audioPath,
|
|
688
|
+
config.outputFormat || 'mp3',
|
|
689
|
+
config.metadata
|
|
690
|
+
);
|
|
691
|
+
|
|
692
|
+
const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
|
|
693
|
+
console.log(`[podcast-composer] Composition complete in ${elapsed}s`);
|
|
694
|
+
|
|
695
|
+
return finalPath;
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
/**
|
|
699
|
+
* Clean up temporary files created during composition.
|
|
700
|
+
*/
|
|
701
|
+
cleanup(): void {
|
|
702
|
+
for (const file of this.tempFiles) {
|
|
703
|
+
try {
|
|
704
|
+
if (existsSync(file)) unlinkSync(file);
|
|
705
|
+
} catch { /* ignore cleanup errors */ }
|
|
706
|
+
}
|
|
707
|
+
this.tempFiles = [];
|
|
708
|
+
console.log('[podcast-composer] Temp files cleaned up');
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
```
|
|
712
|
+
|
|
713
|
+
### Usage Example
|
|
714
|
+
|
|
715
|
+
```typescript
|
|
716
|
+
const composer = new PodcastComposer('./temp_podcast');
|
|
717
|
+
|
|
718
|
+
try {
|
|
719
|
+
const finalPath = await composer.compose({
|
|
720
|
+
segments: [
|
|
721
|
+
{ speaker: 'host', file: 'segments/001_intro.wav', pauseAfterMs: 500 },
|
|
722
|
+
{ speaker: 'guest', file: 'segments/002_response.wav', pauseAfterMs: 300 },
|
|
723
|
+
{ speaker: 'host', file: 'segments/003_question.wav', pauseAfterMs: 400 },
|
|
724
|
+
{ speaker: 'guest', file: 'segments/004_answer.wav', pauseAfterMs: 300 },
|
|
725
|
+
{ speaker: 'host', file: 'segments/005_closing.wav' },
|
|
726
|
+
],
|
|
727
|
+
speakers: {
|
|
728
|
+
host: { name: 'Pastor James', voice: 'en-US-Neural2-D', role: 'host', eqPreset: 'deep', pan: -0.15 },
|
|
729
|
+
guest: { name: 'Dr. Chen', voice: 'en-US-Neural2-F', role: 'guest', eqPreset: 'bright', pan: 0.15 },
|
|
730
|
+
},
|
|
731
|
+
music: {
|
|
732
|
+
intro: 'assets/intro_jingle.mp3',
|
|
733
|
+
outro: 'assets/outro_jingle.mp3',
|
|
734
|
+
bed: 'assets/ambient_bed.mp3',
|
|
735
|
+
bedVolume: 0.12,
|
|
736
|
+
},
|
|
737
|
+
crossfadeDuration: 2,
|
|
738
|
+
defaultPauseMs: 300,
|
|
739
|
+
outputFormat: 'mp3',
|
|
740
|
+
outputPath: './output/episode_042.mp3',
|
|
741
|
+
metadata: {
|
|
742
|
+
title: 'Episode 42: Faith and Technology',
|
|
743
|
+
artist: 'Ministry Podcast',
|
|
744
|
+
album: 'Ministry Podcast Season 3',
|
|
745
|
+
date: '2026',
|
|
746
|
+
genre: 'Podcast',
|
|
747
|
+
comment: 'AI-generated from sermon transcript — produced with Anthropic Claude + Google Gemini TTS',
|
|
748
|
+
},
|
|
749
|
+
});
|
|
750
|
+
|
|
751
|
+
console.log('Final podcast:', finalPath);
|
|
752
|
+
} finally {
|
|
753
|
+
composer.cleanup();
|
|
754
|
+
}
|
|
755
|
+
```
|
|
756
|
+
|
|
757
|
+
---
|
|
758
|
+
|
|
759
|
+
## 4. Speaker Voice Differentiation
|
|
760
|
+
|
|
761
|
+
### EQ Profiles by Role
|
|
762
|
+
|
|
763
|
+
Different EQ treatments create audible distinction between speakers, even when using similar TTS voices.
|
|
764
|
+
|
|
765
|
+
```
|
|
766
|
+
Host (deep):
|
|
767
|
+
+2dB at 200Hz (warmth)
|
|
768
|
+
+1dB at 3kHz (presence)
|
|
769
|
+
Highpass at 60Hz
|
|
770
|
+
Result: Warm, authoritative
|
|
771
|
+
|
|
772
|
+
Guest (bright):
|
|
773
|
+
+3dB at 3.5kHz (clarity)
|
|
774
|
+
+1.5dB at 6kHz (air)
|
|
775
|
+
Highpass at 100Hz
|
|
776
|
+
Result: Clear, articulate
|
|
777
|
+
|
|
778
|
+
Narrator (neutral):
|
|
779
|
+
Highpass at 80Hz
|
|
780
|
+
No boost/cut
|
|
781
|
+
Result: Clean, transparent
|
|
782
|
+
```
|
|
783
|
+
|
|
784
|
+
### FFmpeg EQ Commands
|
|
785
|
+
|
|
786
|
+
```bash
|
|
787
|
+
# Host voice — warm and authoritative
|
|
788
|
+
ffmpeg -i host_raw.wav \
|
|
789
|
+
-af "highpass=f=60,equalizer=f=200:t=q:w=1.0:g=2,equalizer=f=3000:t=q:w=1.5:g=1" \
|
|
790
|
+
host_eq.wav
|
|
791
|
+
|
|
792
|
+
# Guest voice — clear and articulate
|
|
793
|
+
ffmpeg -i guest_raw.wav \
|
|
794
|
+
-af "highpass=f=100,equalizer=f=3500:t=q:w=1.5:g=3,equalizer=f=6000:t=q:w=2.0:g=1.5" \
|
|
795
|
+
guest_eq.wav
|
|
796
|
+
|
|
797
|
+
# Narrator — clean pass
|
|
798
|
+
ffmpeg -i narrator_raw.wav \
|
|
799
|
+
-af "highpass=f=80" \
|
|
800
|
+
narrator_eq.wav
|
|
801
|
+
```
|
|
802
|
+
|
|
803
|
+
### Spatial Separation (Stereo Panning)
|
|
804
|
+
|
|
805
|
+
Subtle panning creates a "two people in a room" feel without being distracting:
|
|
806
|
+
|
|
807
|
+
```bash
|
|
808
|
+
# Pan host slightly left (10-15% — subtle, not jarring)
|
|
809
|
+
ffmpeg -i host.wav -af "pan=stereo|c0=1.0*c0|c1=0.7*c0" host_panned.wav
|
|
810
|
+
|
|
811
|
+
# Pan guest slightly right
|
|
812
|
+
ffmpeg -i guest.wav -af "pan=stereo|c0=0.7*c0|c1=1.0*c0" guest_panned.wav
|
|
813
|
+
|
|
814
|
+
# Narrator stays centered
|
|
815
|
+
# (no panning needed — default center)
|
|
816
|
+
```
|
|
817
|
+
|
|
818
|
+
**Guidelines:**
|
|
819
|
+
- Keep panning subtle: 10-20% off-center maximum
|
|
820
|
+
- Listener should feel spatial presence, not notice panning
|
|
821
|
+
- Mono compatibility: verify the mix sounds good summed to mono
|
|
822
|
+
- Skip panning for single-speaker podcasts or narration
|
|
823
|
+
|
|
824
|
+
### Volume Leveling Across Speakers
|
|
825
|
+
|
|
826
|
+
```bash
|
|
827
|
+
# Measure each speaker's average loudness
|
|
828
|
+
ffmpeg -i host_segments.wav -af ebur128=peak=true -f null NUL 2>&1
|
|
829
|
+
ffmpeg -i guest_segments.wav -af ebur128=peak=true -f null NUL 2>&1
|
|
830
|
+
|
|
831
|
+
# Apply gain adjustment to match target (-16 LUFS for both)
|
|
832
|
+
ffmpeg -i guest_segments.wav -af "volume=2.5dB" guest_leveled.wav
|
|
833
|
+
```
|
|
834
|
+
|
|
835
|
+
---
|
|
836
|
+
|
|
837
|
+
## 5. Production Templates
|
|
838
|
+
|
|
839
|
+
### Template A: Simple Two-Speaker Podcast
|
|
840
|
+
|
|
841
|
+
```
|
|
842
|
+
Timeline:
|
|
843
|
+
| 2s intro | Host intro | Discussion... | Host outro | 2s outro |
|
|
844
|
+
| jingle | (10s) | (variable) | (10s) | jingle |
|
|
845
|
+
^ ^ ^
|
|
846
|
+
crossfade music bed crossfade
|
|
847
|
+
(1.5s) at 10% vol (1.5s)
|
|
848
|
+
```
|
|
849
|
+
|
|
850
|
+
```typescript
|
|
851
|
+
const simpleConfig: CompositionConfig = {
|
|
852
|
+
segments: [
|
|
853
|
+
{ speaker: 'host', file: 'host_intro.wav', pauseAfterMs: 500 },
|
|
854
|
+
// ... discussion segments ...
|
|
855
|
+
{ speaker: 'host', file: 'host_outro.wav' },
|
|
856
|
+
],
|
|
857
|
+
speakers: {
|
|
858
|
+
host: { name: 'Host', voice: 'en-US-Neural2-D', role: 'host', eqPreset: 'deep' },
|
|
859
|
+
guest: { name: 'Guest', voice: 'en-US-Neural2-F', role: 'guest', eqPreset: 'bright' },
|
|
860
|
+
},
|
|
861
|
+
music: {
|
|
862
|
+
intro: 'assets/jingle_2s.mp3',
|
|
863
|
+
outro: 'assets/jingle_2s.mp3',
|
|
864
|
+
bed: 'assets/soft_ambient.mp3',
|
|
865
|
+
bedVolume: 0.10,
|
|
866
|
+
},
|
|
867
|
+
crossfadeDuration: 1.5,
|
|
868
|
+
defaultPauseMs: 300,
|
|
869
|
+
outputFormat: 'mp3',
|
|
870
|
+
outputPath: './output/simple_episode.mp3',
|
|
871
|
+
metadata: { title: 'Episode Title', artist: 'Podcast Name', genre: 'Podcast', date: '2026' },
|
|
872
|
+
};
|
|
873
|
+
```
|
|
874
|
+
|
|
875
|
+
### Template B: Professional Production
|
|
876
|
+
|
|
877
|
+
```
|
|
878
|
+
Timeline:
|
|
879
|
+
| 5s music | Host | Sponsor | Main | Break | Part 2 | Sponsor | Outro | 5s music |
|
|
880
|
+
| intro | welcome | slot | discussion | music | | slot | w/ CTA | outro |
|
|
881
|
+
^ ^ ^
|
|
882
|
+
crossfade 3s music crossfade
|
|
883
|
+
(2.5s) transition (2.5s)
|
|
884
|
+
```
|
|
885
|
+
|
|
886
|
+
```typescript
|
|
887
|
+
const professionalConfig: CompositionConfig = {
|
|
888
|
+
segments: [
|
|
889
|
+
// Act 1: Welcome + Sponsor
|
|
890
|
+
{ speaker: 'host', file: 'seg/welcome.wav', pauseAfterMs: 300 },
|
|
891
|
+
{ speaker: 'host', file: 'seg/sponsor_read.wav', pauseAfterMs: 800 },
|
|
892
|
+
// Act 2: Main discussion
|
|
893
|
+
{ speaker: 'host', file: 'seg/topic_intro.wav', pauseAfterMs: 400 },
|
|
894
|
+
{ speaker: 'guest', file: 'seg/guest_point_1.wav', pauseAfterMs: 300 },
|
|
895
|
+
{ speaker: 'host', file: 'seg/host_response_1.wav', pauseAfterMs: 300 },
|
|
896
|
+
{ speaker: 'guest', file: 'seg/guest_point_2.wav', pauseAfterMs: 300 },
|
|
897
|
+
// Break marker — handled by pauseAfterMs
|
|
898
|
+
{ speaker: 'host', file: 'seg/break_transition.wav', pauseAfterMs: 1000 },
|
|
899
|
+
// Act 3: Part 2 + Outro
|
|
900
|
+
{ speaker: 'host', file: 'seg/part2_intro.wav', pauseAfterMs: 400 },
|
|
901
|
+
{ speaker: 'guest', file: 'seg/guest_point_3.wav', pauseAfterMs: 300 },
|
|
902
|
+
{ speaker: 'host', file: 'seg/sponsor_read_2.wav', pauseAfterMs: 500 },
|
|
903
|
+
{ speaker: 'host', file: 'seg/closing_cta.wav' },
|
|
904
|
+
],
|
|
905
|
+
speakers: {
|
|
906
|
+
host: { name: 'Host', voice: 'en-US-Neural2-D', role: 'host', eqPreset: 'deep', pan: -0.12 },
|
|
907
|
+
guest: { name: 'Guest', voice: 'en-US-Neural2-F', role: 'guest', eqPreset: 'bright', pan: 0.12 },
|
|
908
|
+
},
|
|
909
|
+
music: {
|
|
910
|
+
intro: 'assets/theme_5s.mp3',
|
|
911
|
+
outro: 'assets/theme_5s.mp3',
|
|
912
|
+
bed: 'assets/minimal_beat.mp3',
|
|
913
|
+
bedVolume: 0.08,
|
|
914
|
+
},
|
|
915
|
+
crossfadeDuration: 2.5,
|
|
916
|
+
defaultPauseMs: 300,
|
|
917
|
+
outputFormat: 'mp3',
|
|
918
|
+
outputPath: './output/professional_episode.mp3',
|
|
919
|
+
metadata: {
|
|
920
|
+
title: 'Episode 42: Faith and Technology',
|
|
921
|
+
artist: 'Ministry Podcast',
|
|
922
|
+
album: 'Ministry Podcast Season 3',
|
|
923
|
+
date: '2026',
|
|
924
|
+
genre: 'Podcast',
|
|
925
|
+
comment: 'Produced with Anthropic Claude + Google Gemini TTS',
|
|
926
|
+
},
|
|
927
|
+
};
|
|
928
|
+
```
|
|
929
|
+
|
|
930
|
+
### Template C: Educational Narration
|
|
931
|
+
|
|
932
|
+
```
|
|
933
|
+
Timeline:
|
|
934
|
+
| Ambient | Chapter 1 | chime | Chapter 2 | chime | Summary | Music |
|
|
935
|
+
| fade in | narration | break | narration | break | | fade out |
|
|
936
|
+
^ ^
|
|
937
|
+
3s fade in 5s fade out
|
|
938
|
+
ambient music bed at 10% volume throughout
|
|
939
|
+
```
|
|
940
|
+
|
|
941
|
+
```typescript
|
|
942
|
+
const educationalConfig: CompositionConfig = {
|
|
943
|
+
segments: [
|
|
944
|
+
{ speaker: 'narrator', file: 'seg/chapter1_intro.wav', pauseAfterMs: 200 },
|
|
945
|
+
{ speaker: 'narrator', file: 'seg/chapter1_body.wav', pauseAfterMs: 1000 },
|
|
946
|
+
// Chapter break — long pause signals section change
|
|
947
|
+
{ speaker: 'narrator', file: 'seg/chapter2_intro.wav', pauseAfterMs: 200 },
|
|
948
|
+
{ speaker: 'narrator', file: 'seg/chapter2_body.wav', pauseAfterMs: 1000 },
|
|
949
|
+
{ speaker: 'narrator', file: 'seg/summary.wav' },
|
|
950
|
+
],
|
|
951
|
+
speakers: {
|
|
952
|
+
narrator: { name: 'Narrator', voice: 'en-US-Neural2-D', role: 'narrator', eqPreset: 'neutral' },
|
|
953
|
+
},
|
|
954
|
+
music: {
|
|
955
|
+
bed: 'assets/ambient_piano.mp3',
|
|
956
|
+
bedVolume: 0.10,
|
|
957
|
+
},
|
|
958
|
+
crossfadeDuration: 2,
|
|
959
|
+
defaultPauseMs: 200,
|
|
960
|
+
outputFormat: 'mp3',
|
|
961
|
+
outputPath: './output/educational_narration.mp3',
|
|
962
|
+
metadata: {
|
|
963
|
+
title: 'Understanding Grace: Chapter 1-2',
|
|
964
|
+
artist: 'Ministry Teaching Series',
|
|
965
|
+
genre: 'Podcast',
|
|
966
|
+
date: '2026',
|
|
967
|
+
comment: 'AI narration from source document — Anthropic Claude',
|
|
968
|
+
},
|
|
969
|
+
};
|
|
970
|
+
```
|
|
971
|
+
|
|
972
|
+
---
|
|
973
|
+
|
|
974
|
+
## 6. Sound Design Elements
|
|
975
|
+
|
|
976
|
+
### Sourcing Royalty-Free Audio
|
|
977
|
+
|
|
978
|
+
| Element | Source | License | Notes |
|
|
979
|
+
|---------|--------|---------|-------|
|
|
980
|
+
| Intro/outro music | [Pixabay Audio](https://pixabay.com/music/) | Pixabay License (free, no attribution) | Search "podcast intro", filter by <10s |
|
|
981
|
+
| Transition sounds | [Freesound.org](https://freesound.org) | CC0 / CC-BY | Search "whoosh", "chime", "transition" |
|
|
982
|
+
| Background ambient | [Free Music Archive](https://freemusicarchive.org) | CC-BY / CC0 | Search "ambient", "lo-fi", filter instrumental |
|
|
983
|
+
| Sound effects | [Mixkit](https://mixkit.co/free-sound-effects/) | Free license | Clean UI, categorized well |
|
|
984
|
+
| Chapter chimes | [Zapsplat](https://www.zapsplat.com) | Free with attribution | Large SFX library |
|
|
985
|
+
|
|
986
|
+
### Recommended Sound Levels
|
|
987
|
+
|
|
988
|
+
```
|
|
989
|
+
Element Volume Level Notes
|
|
990
|
+
------- ------------ -----
|
|
991
|
+
Speech (primary) 0 dB (ref) Normalized to -16 LUFS
|
|
992
|
+
Background music bed -18 to -24 dB 10-15% of speech level
|
|
993
|
+
Intro/outro jingle -6 to -3 dB Prominent but not jarring
|
|
994
|
+
Transition whoosh/chime -12 to -9 dB Noticeable but brief
|
|
995
|
+
Ambient room tone -30 dB Barely perceptible
|
|
996
|
+
```
|
|
997
|
+
|
|
998
|
+
### FFmpeg Commands for Sound Design
|
|
999
|
+
|
|
1000
|
+
```bash
|
|
1001
|
+
# Add a short transition chime between sections
|
|
1002
|
+
ffmpeg -i before.wav -i chime.wav -i after.wav \
|
|
1003
|
+
-filter_complex \
|
|
1004
|
+
"[1:a]volume=0.3,adelay=0|0[chime]; \
|
|
1005
|
+
[0:a][chime]amix=inputs=2:duration=first[with_chime]; \
|
|
1006
|
+
[with_chime][2:a]concat=n=2:v=0:a=1" \
|
|
1007
|
+
with_transition.wav
|
|
1008
|
+
|
|
1009
|
+
# Fade music from full volume to bed volume at speech start
|
|
1010
|
+
ffmpeg -i music.mp3 \
|
|
1011
|
+
-af "volume=1.0,afade=t=out:st=3:d=2,volume=0.12" \
|
|
1012
|
+
music_ducked.wav
|
|
1013
|
+
|
|
1014
|
+
# Create a "room tone" ambient layer (pink noise, very quiet)
|
|
1015
|
+
ffmpeg -f lavfi -i "anoisesrc=d=300:c=pink:r=44100:a=0.005" \
|
|
1016
|
+
-c:a pcm_s16le room_tone_5min.wav
|
|
1017
|
+
```
|
|
1018
|
+
|
|
1019
|
+
---
|
|
1020
|
+
|
|
1021
|
+
## 7. Metadata Tagging
|
|
1022
|
+
|
|
1023
|
+
### Full ID3 Tag Set for Podcast Distribution
|
|
1024
|
+
|
|
1025
|
+
```bash
|
|
1026
|
+
# Complete metadata for podcast episode
|
|
1027
|
+
ffmpeg -i podcast.mp3 \
|
|
1028
|
+
-metadata title="Episode 42: Faith and Technology" \
|
|
1029
|
+
-metadata artist="Ministry Podcast" \
|
|
1030
|
+
-metadata album="Ministry Podcast Season 3" \
|
|
1031
|
+
-metadata album_artist="Ministry Podcast" \
|
|
1032
|
+
-metadata date="2026" \
|
|
1033
|
+
-metadata genre="Podcast" \
|
|
1034
|
+
-metadata track="42" \
|
|
1035
|
+
-metadata comment="AI-generated from sermon transcript — Anthropic Claude + Google Gemini TTS" \
|
|
1036
|
+
-metadata publisher="Ministry Name" \
|
|
1037
|
+
-metadata copyright="2026 Ministry Name" \
|
|
1038
|
+
-metadata language="eng" \
|
|
1039
|
+
-c copy \
|
|
1040
|
+
tagged_podcast.mp3
|
|
1041
|
+
```
|
|
1042
|
+
|
|
1043
|
+
### Embed Cover Art
|
|
1044
|
+
|
|
1045
|
+
```bash
|
|
1046
|
+
# Add cover art to MP3 (ID3v2 embedded image)
|
|
1047
|
+
ffmpeg -i podcast.mp3 -i cover_art.jpg \
|
|
1048
|
+
-map 0:a -map 1:0 \
|
|
1049
|
+
-c copy -id3v2_version 3 \
|
|
1050
|
+
-metadata:s:v title="Album cover" \
|
|
1051
|
+
-metadata:s:v comment="Cover (front)" \
|
|
1052
|
+
podcast_with_cover.mp3
|
|
1053
|
+
|
|
1054
|
+
# Add cover art to M4A/AAC
|
|
1055
|
+
ffmpeg -i podcast.m4a -i cover_art.jpg \
|
|
1056
|
+
-map 0:a -map 1:0 \
|
|
1057
|
+
-c:a copy -c:v mjpeg \
|
|
1058
|
+
-disposition:v attached_pic \
|
|
1059
|
+
podcast_with_cover.m4a
|
|
1060
|
+
```
|
|
1061
|
+
|
|
1062
|
+
### Cover Art Specifications
|
|
1063
|
+
|
|
1064
|
+
| Platform | Minimum | Recommended | Max | Format |
|
|
1065
|
+
|----------|---------|-------------|-----|--------|
|
|
1066
|
+
| Apple Podcasts | 1400x1400 | 3000x3000 | 3000x3000 | JPEG/PNG |
|
|
1067
|
+
| Spotify | 640x640 | 3000x3000 | 3000x3000 | JPEG |
|
|
1068
|
+
| Google Podcasts | 600x600 | 1200x1200 | — | JPEG/PNG |
|
|
1069
|
+
| ID3 embed | — | 500x500 | 1000x1000 | JPEG (smaller file size) |
|
|
1070
|
+
|
|
1071
|
+
### Validate Metadata
|
|
1072
|
+
|
|
1073
|
+
```bash
|
|
1074
|
+
# Read back all metadata from the file
|
|
1075
|
+
ffprobe -i podcast.mp3 -show_format -show_streams -v quiet -print_format json
|
|
1076
|
+
|
|
1077
|
+
# Quick check — just metadata tags
|
|
1078
|
+
ffprobe -i podcast.mp3 -show_entries format_tags -v quiet -of default=noprint_wrappers=1
|
|
1079
|
+
```
|
|
1080
|
+
|
|
1081
|
+
---
|
|
1082
|
+
|
|
1083
|
+
## 8. Quality Verification Checklist
|
|
1084
|
+
|
|
1085
|
+
After composition, verify the output meets podcast distribution standards:
|
|
1086
|
+
|
|
1087
|
+
```bash
|
|
1088
|
+
# 1. Measure loudness (target: -16 LUFS, peak below -1.5 dBTP)
|
|
1089
|
+
ffmpeg -i final_podcast.mp3 -af ebur128=peak=true -f null NUL 2>&1
|
|
1090
|
+
|
|
1091
|
+
# 2. Check duration matches expected length
|
|
1092
|
+
ffprobe -i final_podcast.mp3 -show_entries format=duration -v quiet -of csv="p=0"
|
|
1093
|
+
|
|
1094
|
+
# 3. Verify sample rate and bitrate
|
|
1095
|
+
ffprobe -i final_podcast.mp3 -show_entries stream=sample_rate,bit_rate -v quiet
|
|
1096
|
+
|
|
1097
|
+
# 4. Generate waveform for visual inspection
|
|
1098
|
+
ffmpeg -i final_podcast.mp3 -lavfi showwavespic=s=1920x200:colors=0x2563EB waveform.png
|
|
1099
|
+
|
|
1100
|
+
# 5. Generate spectrogram to check for artifacts
|
|
1101
|
+
ffmpeg -i final_podcast.mp3 -lavfi showspectrumpic=s=1920x1080:mode=combined:scale=log spectrogram.png
|
|
1102
|
+
```
|
|
1103
|
+
|
|
1104
|
+
### Automated Quality Gate (Node.js)
|
|
1105
|
+
|
|
1106
|
+
```typescript
|
|
1107
|
+
interface QualityResult {
|
|
1108
|
+
lufs: number;
|
|
1109
|
+
truePeak: number;
|
|
1110
|
+
lra: number;
|
|
1111
|
+
duration: number;
|
|
1112
|
+
sampleRate: number;
|
|
1113
|
+
pass: boolean;
|
|
1114
|
+
issues: string[];
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
async function verifyPodcastQuality(filePath: string): Promise<QualityResult> {
|
|
1118
|
+
const issues: string[] = [];
|
|
1119
|
+
|
|
1120
|
+
// Measure loudness (capture stderr where FFmpeg writes the JSON)
|
|
1121
|
+
let measureOutput: string;
|
|
1122
|
+
try {
|
|
1123
|
+
execFileSync('ffmpeg', [
|
|
1124
|
+
'-i', filePath,
|
|
1125
|
+
'-af', 'loudnorm=I=-16:TP=-1.5:LRA=11:print_format=json',
|
|
1126
|
+
'-f', 'null', 'NUL'
|
|
1127
|
+
], { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] });
|
|
1128
|
+
measureOutput = '';
|
|
1129
|
+
} catch (err: any) {
|
|
1130
|
+
measureOutput = err.stderr || '';
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
const jsonMatch = measureOutput.match(/\{[\s\S]*?"input_i"[\s\S]*?\}/);
|
|
1134
|
+
const measured = jsonMatch ? JSON.parse(jsonMatch[0]) : null;
|
|
1135
|
+
|
|
1136
|
+
if (!measured) {
|
|
1137
|
+
return {
|
|
1138
|
+
lufs: 0, truePeak: 0, lra: 0, duration: 0, sampleRate: 0,
|
|
1139
|
+
pass: false, issues: ['Could not measure loudness']
|
|
1140
|
+
};
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
const lufs = parseFloat(measured.input_i);
|
|
1144
|
+
const truePeak = parseFloat(measured.input_tp);
|
|
1145
|
+
const lra = parseFloat(measured.input_lra);
|
|
1146
|
+
|
|
1147
|
+
// Get duration and sample rate
|
|
1148
|
+
const probeOutput = execFileSync('ffprobe', [
|
|
1149
|
+
'-i', filePath,
|
|
1150
|
+
'-show_entries', 'format=duration:stream=sample_rate',
|
|
1151
|
+
'-v', 'quiet', '-of', 'json'
|
|
1152
|
+
], { encoding: 'utf-8' });
|
|
1153
|
+
|
|
1154
|
+
const probe = JSON.parse(probeOutput);
|
|
1155
|
+
const duration = parseFloat(probe.format?.duration || '0');
|
|
1156
|
+
const sampleRate = parseInt(probe.streams?.[0]?.sample_rate || '0', 10);
|
|
1157
|
+
|
|
1158
|
+
// Quality checks
|
|
1159
|
+
if (lufs < -18 || lufs > -14) {
|
|
1160
|
+
issues.push(`Loudness out of range: ${lufs} LUFS (target: -16 +/- 2)`);
|
|
1161
|
+
}
|
|
1162
|
+
if (truePeak > -1.0) {
|
|
1163
|
+
issues.push(`True peak too high: ${truePeak} dBTP (max: -1.0)`);
|
|
1164
|
+
}
|
|
1165
|
+
if (lra > 15) {
|
|
1166
|
+
issues.push(`Dynamic range too wide: ${lra} LU (max: 15)`);
|
|
1167
|
+
}
|
|
1168
|
+
if (lra < 3) {
|
|
1169
|
+
issues.push(`Dynamic range too narrow: ${lra} LU (min: 3)`);
|
|
1170
|
+
}
|
|
1171
|
+
if (sampleRate < 44100) {
|
|
1172
|
+
issues.push(`Sample rate too low: ${sampleRate} Hz (min: 44100)`);
|
|
1173
|
+
}
|
|
1174
|
+
if (duration < 10) {
|
|
1175
|
+
issues.push(`Duration suspiciously short: ${duration}s`);
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
const pass = issues.length === 0;
|
|
1179
|
+
|
|
1180
|
+
console.log(
|
|
1181
|
+
`[quality-check] ${pass ? 'PASS' : 'FAIL'}` +
|
|
1182
|
+
` — ${lufs.toFixed(1)} LUFS, ${truePeak.toFixed(1)} dBTP, ${lra.toFixed(1)} LU`
|
|
1183
|
+
);
|
|
1184
|
+
if (!pass) issues.forEach(i => console.warn(` - ${i}`));
|
|
1185
|
+
|
|
1186
|
+
return { lufs, truePeak, lra, duration, sampleRate, pass, issues };
|
|
1187
|
+
}
|
|
1188
|
+
```
|
|
1189
|
+
|
|
1190
|
+
---
|
|
1191
|
+
|
|
1192
|
+
## 9. Advanced Techniques
|
|
1193
|
+
|
|
1194
|
+
### Batch Episode Production
|
|
1195
|
+
|
|
1196
|
+
Generate multiple episodes from a script directory:
|
|
1197
|
+
|
|
1198
|
+
```typescript
|
|
1199
|
+
import { readdir, readFile } from 'fs/promises';
|
|
1200
|
+
|
|
1201
|
+
async function batchProduceEpisodes(
|
|
1202
|
+
scriptDir: string,
|
|
1203
|
+
outputDir: string
|
|
1204
|
+
): Promise<void> {
|
|
1205
|
+
const files = await readdir(scriptDir);
|
|
1206
|
+
const scripts = files.filter(f => f.endsWith('.json'));
|
|
1207
|
+
|
|
1208
|
+
console.log(`[batch] Found ${scripts.length} episode scripts`);
|
|
1209
|
+
|
|
1210
|
+
for (const scriptFile of scripts) {
|
|
1211
|
+
const scriptPath = join(scriptDir, scriptFile);
|
|
1212
|
+
const raw = await readFile(scriptPath, 'utf-8');
|
|
1213
|
+
const config: CompositionConfig = JSON.parse(raw);
|
|
1214
|
+
|
|
1215
|
+
config.outputPath = join(outputDir, scriptFile.replace('.json', '.mp3'));
|
|
1216
|
+
|
|
1217
|
+
const composer = new PodcastComposer(join('./temp', scriptFile));
|
|
1218
|
+
try {
|
|
1219
|
+
const result = await composer.compose(config);
|
|
1220
|
+
const quality = await verifyPodcastQuality(result);
|
|
1221
|
+
console.log(`[batch] ${scriptFile}: ${quality.pass ? 'PASS' : 'FAIL'}`);
|
|
1222
|
+
} finally {
|
|
1223
|
+
composer.cleanup();
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
```
|
|
1228
|
+
|
|
1229
|
+
### Dynamic Music Ducking with Silence Detection
|
|
1230
|
+
|
|
1231
|
+
Instead of static volume, detect speech segments and duck music automatically:
|
|
1232
|
+
|
|
1233
|
+
```bash
|
|
1234
|
+
# Detect silent regions in speech track (pauses where music can be louder)
|
|
1235
|
+
ffmpeg -i speech.wav -af silencedetect=noise=-35dB:d=0.5 -f null NUL 2>&1
|
|
1236
|
+
|
|
1237
|
+
# Use sidechaincompress for real-time ducking
|
|
1238
|
+
# Music plays at full volume during silence, ducks under speech
|
|
1239
|
+
ffmpeg -i speech.wav -i music.wav \
|
|
1240
|
+
-filter_complex \
|
|
1241
|
+
"[1:a]volume=0.25[music]; \
|
|
1242
|
+
[music][0:a]sidechaincompress=threshold=0.02:ratio=8:attack=100:release=800:knee=3[ducked]; \
|
|
1243
|
+
[0:a][ducked]amix=inputs=2:duration=first:weights=1 0.8" \
|
|
1244
|
+
auto_ducked.wav
|
|
1245
|
+
```
|
|
1246
|
+
|
|
1247
|
+
### Chapter Markers for Podcast Players
|
|
1248
|
+
|
|
1249
|
+
Some podcast players support chapter markers (MP4/M4A format):
|
|
1250
|
+
|
|
1251
|
+
```bash
|
|
1252
|
+
# Create chapter metadata file (chapters.txt)
|
|
1253
|
+
# Format: ;FFMETADATA1
|
|
1254
|
+
cat > chapters.txt << 'CHAPTEREOF'
|
|
1255
|
+
;FFMETADATA1
|
|
1256
|
+
[CHAPTER]
|
|
1257
|
+
TIMEBASE=1/1000
|
|
1258
|
+
START=0
|
|
1259
|
+
END=5000
|
|
1260
|
+
title=Intro
|
|
1261
|
+
|
|
1262
|
+
[CHAPTER]
|
|
1263
|
+
TIMEBASE=1/1000
|
|
1264
|
+
START=5000
|
|
1265
|
+
END=120000
|
|
1266
|
+
title=Welcome & Overview
|
|
1267
|
+
|
|
1268
|
+
[CHAPTER]
|
|
1269
|
+
TIMEBASE=1/1000
|
|
1270
|
+
START=120000
|
|
1271
|
+
END=600000
|
|
1272
|
+
title=Main Discussion
|
|
1273
|
+
|
|
1274
|
+
[CHAPTER]
|
|
1275
|
+
TIMEBASE=1/1000
|
|
1276
|
+
START=600000
|
|
1277
|
+
END=660000
|
|
1278
|
+
title=Closing Thoughts
|
|
1279
|
+
CHAPTEREOF
|
|
1280
|
+
|
|
1281
|
+
# Apply chapters to M4A file
|
|
1282
|
+
ffmpeg -i podcast.m4a -i chapters.txt -map_metadata 1 -c copy podcast_with_chapters.m4a
|
|
1283
|
+
```
|
|
1284
|
+
|
|
1285
|
+
### Podcast RSS Feed Integration
|
|
1286
|
+
|
|
1287
|
+
After producing the audio, generate the RSS enclosure entry:
|
|
1288
|
+
|
|
1289
|
+
```typescript
|
|
1290
|
+
import { statSync } from 'fs';
|
|
1291
|
+
|
|
1292
|
+
function generateRSSEnclosure(filePath: string, baseUrl: string): string {
|
|
1293
|
+
const stats = statSync(filePath);
|
|
1294
|
+
const filename = basename(filePath);
|
|
1295
|
+
const duration = getAudioDuration(filePath);
|
|
1296
|
+
const minutes = Math.floor(duration / 60);
|
|
1297
|
+
const seconds = Math.floor(duration % 60);
|
|
1298
|
+
|
|
1299
|
+
return `
|
|
1300
|
+
<item>
|
|
1301
|
+
<title>${filename.replace(/\.[^.]+$/, '').replace(/_/g, ' ')}</title>
|
|
1302
|
+
<enclosure url="${baseUrl}/${filename}" length="${stats.size}" type="audio/mpeg" />
|
|
1303
|
+
<itunes:duration>${minutes}:${seconds.toString().padStart(2, '0')}</itunes:duration>
|
|
1304
|
+
<pubDate>${new Date().toUTCString()}</pubDate>
|
|
1305
|
+
</item>
|
|
1306
|
+
`.trim();
|
|
1307
|
+
}
|
|
1308
|
+
```
|
|
1309
|
+
|
|
1310
|
+
---
|
|
1311
|
+
|
|
1312
|
+
## 10. Troubleshooting
|
|
1313
|
+
|
|
1314
|
+
### Common Issues
|
|
1315
|
+
|
|
1316
|
+
| Problem | Cause | Fix |
|
|
1317
|
+
|---------|-------|-----|
|
|
1318
|
+
| "Discarding samples" warning | Sample rate mismatch between segments | Normalize all inputs to same sample rate first |
|
|
1319
|
+
| Clicks between concatenated segments | Hard cut at non-zero crossing | Add 5ms crossfade or 10ms fade-out/fade-in at boundaries |
|
|
1320
|
+
| Music too loud / drowns speech | `amix` normalizes by default | Use `weights` parameter or set music `volume` lower |
|
|
1321
|
+
| Output louder than expected | `amix` gain normalization | Add `normalize=0` to `amix` filter |
|
|
1322
|
+
| Crossfade produces silence gap | Duration > segment length | Crossfade duration must be shorter than shortest segment |
|
|
1323
|
+
| Mono/stereo mismatch | Mixing mono speech with stereo music | Convert all to same channel layout before mixing |
|
|
1324
|
+
|
|
1325
|
+
### Fix Clicks at Boundaries
|
|
1326
|
+
|
|
1327
|
+
```bash
|
|
1328
|
+
# Add 10ms fade-out to end of each segment before concatenation
|
|
1329
|
+
ffmpeg -i segment.wav -af "afade=t=out:st=DURATION_MINUS_0.01:d=0.01" segment_clean.wav
|
|
1330
|
+
|
|
1331
|
+
# Or add 5ms crossfade between every pair during concatenation
|
|
1332
|
+
# (handled by the PodcastComposer class automatically when crossfade > 0)
|
|
1333
|
+
```
|
|
1334
|
+
|
|
1335
|
+
### Fix amix Volume Normalization
|
|
1336
|
+
|
|
1337
|
+
```bash
|
|
1338
|
+
# Default amix divides volume by number of inputs — speech gets quieter
|
|
1339
|
+
# Fix: use weights to keep speech at full volume
|
|
1340
|
+
ffmpeg -i speech.wav -i music.wav \
|
|
1341
|
+
-filter_complex "[1:a]volume=0.12[music];[0:a][music]amix=inputs=2:duration=first:weights=1 1:normalize=0" \
|
|
1342
|
+
output.mp3
|
|
1343
|
+
```
|
|
1344
|
+
|
|
1345
|
+
---
|
|
1346
|
+
|
|
1347
|
+
## References
|
|
1348
|
+
|
|
1349
|
+
- FFmpeg filter documentation: https://ffmpeg.org/ffmpeg-filters.html
|
|
1350
|
+
- EBU R128 loudness standard: https://tech.ebu.ch/docs/r/r128.pdf
|
|
1351
|
+
- Apple Podcasts requirements: https://podcasters.apple.com/support/823
|
|
1352
|
+
- Spotify podcast specs: https://podcasters.spotify.com/resources
|
|
1353
|
+
- FireRedTTS-2: arXiv 2509.02020 (Sep 2025)
|
|
1354
|
+
- DialoSpeech: arXiv 2510.08373 (Oct 2025)
|
|
1355
|
+
- Related skills: [ffmpeg-command-generator.md](ffmpeg-command-generator.md), [audio-enhancement-pipeline.md](audio-enhancement-pipeline.md), [transcription-pipeline-selector.md](transcription-pipeline-selector.md), [content-repurposing-pipeline.md](content-repurposing-pipeline.md)
|