content-foundry 1.0.0__tar.gz

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.
Files changed (127) hide show
  1. content_foundry-1.0.0/LICENSE +21 -0
  2. content_foundry-1.0.0/PKG-INFO +176 -0
  3. content_foundry-1.0.0/README.md +81 -0
  4. content_foundry-1.0.0/pyproject.toml +158 -0
  5. content_foundry-1.0.0/setup.cfg +4 -0
  6. content_foundry-1.0.0/src/content_foundry/__init__.py +4 -0
  7. content_foundry-1.0.0/src/content_foundry/agents/__init__.py +36 -0
  8. content_foundry-1.0.0/src/content_foundry/agents/brainstorm.py +136 -0
  9. content_foundry-1.0.0/src/content_foundry/agents/broll_director.py +93 -0
  10. content_foundry-1.0.0/src/content_foundry/agents/data_fetcher.py +114 -0
  11. content_foundry-1.0.0/src/content_foundry/agents/distill.py +108 -0
  12. content_foundry-1.0.0/src/content_foundry/agents/idea_miner.py +214 -0
  13. content_foundry-1.0.0/src/content_foundry/agents/instruction_planner.py +89 -0
  14. content_foundry-1.0.0/src/content_foundry/agents/judge.py +437 -0
  15. content_foundry-1.0.0/src/content_foundry/agents/judge_checks.py +337 -0
  16. content_foundry-1.0.0/src/content_foundry/agents/publisher.py +191 -0
  17. content_foundry-1.0.0/src/content_foundry/agents/renderer.py +146 -0
  18. content_foundry-1.0.0/src/content_foundry/agents/research.py +265 -0
  19. content_foundry-1.0.0/src/content_foundry/agents/scene_image_director.py +69 -0
  20. content_foundry-1.0.0/src/content_foundry/agents/script_generator.py +910 -0
  21. content_foundry-1.0.0/src/content_foundry/agents/thumbnail_director.py +80 -0
  22. content_foundry-1.0.0/src/content_foundry/agents/visuals.py +956 -0
  23. content_foundry-1.0.0/src/content_foundry/agents/voiceover.py +127 -0
  24. content_foundry-1.0.0/src/content_foundry/cli.py +524 -0
  25. content_foundry-1.0.0/src/content_foundry/config.py +797 -0
  26. content_foundry-1.0.0/src/content_foundry/data/sounds/apple_notification.mp3 +0 -0
  27. content_foundry-1.0.0/src/content_foundry/data/sounds/awww.mp3 +0 -0
  28. content_foundry-1.0.0/src/content_foundry/data/sounds/bell.mp3 +0 -0
  29. content_foundry-1.0.0/src/content_foundry/data/sounds/camera_flash.mp3 +0 -0
  30. content_foundry-1.0.0/src/content_foundry/data/sounds/camera_shutter.mp3 +0 -0
  31. content_foundry-1.0.0/src/content_foundry/data/sounds/cash_register.mp3 +0 -0
  32. content_foundry-1.0.0/src/content_foundry/data/sounds/click.mp3 +0 -0
  33. content_foundry-1.0.0/src/content_foundry/data/sounds/game_menu.mp3 +0 -0
  34. content_foundry-1.0.0/src/content_foundry/data/sounds/pop.mp3 +0 -0
  35. content_foundry-1.0.0/src/content_foundry/data/sounds/whoosh.mp3 +0 -0
  36. content_foundry-1.0.0/src/content_foundry/data/sounds/wrong_answer.mp3 +0 -0
  37. content_foundry-1.0.0/src/content_foundry/datasources/__init__.py +8 -0
  38. content_foundry-1.0.0/src/content_foundry/datasources/adzuna.py +98 -0
  39. content_foundry-1.0.0/src/content_foundry/datasources/base.py +22 -0
  40. content_foundry-1.0.0/src/content_foundry/datasources/bls.py +66 -0
  41. content_foundry-1.0.0/src/content_foundry/datasources/layoffs.py +73 -0
  42. content_foundry-1.0.0/src/content_foundry/datasources/news.py +70 -0
  43. content_foundry-1.0.0/src/content_foundry/datasources/registry.py +100 -0
  44. content_foundry-1.0.0/src/content_foundry/datasources/search.py +359 -0
  45. content_foundry-1.0.0/src/content_foundry/errors.py +61 -0
  46. content_foundry-1.0.0/src/content_foundry/logging.py +88 -0
  47. content_foundry-1.0.0/src/content_foundry/models/__init__.py +50 -0
  48. content_foundry-1.0.0/src/content_foundry/models/data_brief.py +44 -0
  49. content_foundry-1.0.0/src/content_foundry/models/ideas.py +53 -0
  50. content_foundry-1.0.0/src/content_foundry/models/judge_report.py +47 -0
  51. content_foundry-1.0.0/src/content_foundry/models/provenance.py +27 -0
  52. content_foundry-1.0.0/src/content_foundry/models/publish.py +30 -0
  53. content_foundry-1.0.0/src/content_foundry/models/research.py +42 -0
  54. content_foundry-1.0.0/src/content_foundry/models/run.py +62 -0
  55. content_foundry-1.0.0/src/content_foundry/models/script.py +48 -0
  56. content_foundry-1.0.0/src/content_foundry/models/signals.py +34 -0
  57. content_foundry-1.0.0/src/content_foundry/models/video.py +24 -0
  58. content_foundry-1.0.0/src/content_foundry/models/visuals.py +42 -0
  59. content_foundry-1.0.0/src/content_foundry/models/voiceover.py +35 -0
  60. content_foundry-1.0.0/src/content_foundry/notifications/__init__.py +16 -0
  61. content_foundry-1.0.0/src/content_foundry/notifications/base.py +20 -0
  62. content_foundry-1.0.0/src/content_foundry/notifications/credit_monitor.py +73 -0
  63. content_foundry-1.0.0/src/content_foundry/notifications/factory.py +42 -0
  64. content_foundry-1.0.0/src/content_foundry/notifications/telegram.py +22 -0
  65. content_foundry-1.0.0/src/content_foundry/persistence/__init__.py +9 -0
  66. content_foundry-1.0.0/src/content_foundry/persistence/db.py +50 -0
  67. content_foundry-1.0.0/src/content_foundry/persistence/repository.py +239 -0
  68. content_foundry-1.0.0/src/content_foundry/persistence/schema.py +142 -0
  69. content_foundry-1.0.0/src/content_foundry/pipeline/__init__.py +22 -0
  70. content_foundry-1.0.0/src/content_foundry/pipeline/artifacts.py +207 -0
  71. content_foundry-1.0.0/src/content_foundry/pipeline/orchestrator.py +1110 -0
  72. content_foundry-1.0.0/src/content_foundry/pipeline/package.py +96 -0
  73. content_foundry-1.0.0/src/content_foundry/pipeline/stages.py +32 -0
  74. content_foundry-1.0.0/src/content_foundry/pipeline/topic_relevance.py +85 -0
  75. content_foundry-1.0.0/src/content_foundry/production/__init__.py +1 -0
  76. content_foundry-1.0.0/src/content_foundry/production/affiliate.py +497 -0
  77. content_foundry-1.0.0/src/content_foundry/production/captions.py +98 -0
  78. content_foundry-1.0.0/src/content_foundry/production/end_screen.py +146 -0
  79. content_foundry-1.0.0/src/content_foundry/production/like_nudge.py +141 -0
  80. content_foundry-1.0.0/src/content_foundry/production/overlay.py +60 -0
  81. content_foundry-1.0.0/src/content_foundry/production/seo.py +244 -0
  82. content_foundry-1.0.0/src/content_foundry/production/sound_design.py +67 -0
  83. content_foundry-1.0.0/src/content_foundry/production/subscribe.py +129 -0
  84. content_foundry-1.0.0/src/content_foundry/production/timebox.py +48 -0
  85. content_foundry-1.0.0/src/content_foundry/production/timeline.py +48 -0
  86. content_foundry-1.0.0/src/content_foundry/prompts/__init__.py +30 -0
  87. content_foundry-1.0.0/src/content_foundry/prompts/affiliate_queries.system.txt +18 -0
  88. content_foundry-1.0.0/src/content_foundry/prompts/brainstorm.system.txt +26 -0
  89. content_foundry-1.0.0/src/content_foundry/prompts/broll_director.system.txt +69 -0
  90. content_foundry-1.0.0/src/content_foundry/prompts/instruction_planner.system.txt +45 -0
  91. content_foundry-1.0.0/src/content_foundry/prompts/judge.rubric.txt +53 -0
  92. content_foundry-1.0.0/src/content_foundry/prompts/judge.system.txt +62 -0
  93. content_foundry-1.0.0/src/content_foundry/prompts/research.system.txt +34 -0
  94. content_foundry-1.0.0/src/content_foundry/prompts/scene_image_director.system.txt +75 -0
  95. content_foundry-1.0.0/src/content_foundry/prompts/script_generator.system.txt +296 -0
  96. content_foundry-1.0.0/src/content_foundry/prompts/thumbnail_director.system.txt +9 -0
  97. content_foundry-1.0.0/src/content_foundry/providers/__init__.py +257 -0
  98. content_foundry-1.0.0/src/content_foundry/providers/anthropic_provider.py +64 -0
  99. content_foundry-1.0.0/src/content_foundry/providers/base.py +103 -0
  100. content_foundry-1.0.0/src/content_foundry/providers/broll.py +347 -0
  101. content_foundry-1.0.0/src/content_foundry/providers/faceid.py +181 -0
  102. content_foundry-1.0.0/src/content_foundry/providers/faceswap.py +261 -0
  103. content_foundry-1.0.0/src/content_foundry/providers/fallback.py +70 -0
  104. content_foundry-1.0.0/src/content_foundry/providers/google_provider.py +115 -0
  105. content_foundry-1.0.0/src/content_foundry/providers/image.py +193 -0
  106. content_foundry-1.0.0/src/content_foundry/providers/local_provider.py +72 -0
  107. content_foundry-1.0.0/src/content_foundry/providers/openai_provider.py +64 -0
  108. content_foundry-1.0.0/src/content_foundry/providers/render_backend.py +509 -0
  109. content_foundry-1.0.0/src/content_foundry/providers/sfx.py +107 -0
  110. content_foundry-1.0.0/src/content_foundry/providers/text_normalize.py +87 -0
  111. content_foundry-1.0.0/src/content_foundry/providers/tiering.py +26 -0
  112. content_foundry-1.0.0/src/content_foundry/providers/tts.py +519 -0
  113. content_foundry-1.0.0/src/content_foundry/providers/youtube.py +230 -0
  114. content_foundry-1.0.0/src/content_foundry/providers/youtube_data.py +183 -0
  115. content_foundry-1.0.0/src/content_foundry/py.typed +0 -0
  116. content_foundry-1.0.0/src/content_foundry/safeguards/__init__.py +29 -0
  117. content_foundry-1.0.0/src/content_foundry/safeguards/disclosure.py +65 -0
  118. content_foundry-1.0.0/src/content_foundry/safeguards/grounding.py +65 -0
  119. content_foundry-1.0.0/src/content_foundry/scheduler.py +40 -0
  120. content_foundry-1.0.0/src/content_foundry/templates/__init__.py +81 -0
  121. content_foundry-1.0.0/src/content_foundry/templates/definitions.py +108 -0
  122. content_foundry-1.0.0/src/content_foundry.egg-info/PKG-INFO +176 -0
  123. content_foundry-1.0.0/src/content_foundry.egg-info/SOURCES.txt +125 -0
  124. content_foundry-1.0.0/src/content_foundry.egg-info/dependency_links.txt +1 -0
  125. content_foundry-1.0.0/src/content_foundry.egg-info/entry_points.txt +2 -0
  126. content_foundry-1.0.0/src/content_foundry.egg-info/requires.txt +80 -0
  127. content_foundry-1.0.0/src/content_foundry.egg-info/top_level.txt +1 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Pranshu Mishra
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,176 @@
1
+ Metadata-Version: 2.4
2
+ Name: content-foundry
3
+ Version: 1.0.0
4
+ Summary: A resumable multi-agent pipeline that researches, writes, judges, narrates, and renders short-form YouTube videos for any niche.
5
+ Author-email: Pranshu Mishra <pranshumshr.04@gmail.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/pranshu97/content-foundry
8
+ Project-URL: Repository, https://github.com/pranshu97/content-foundry
9
+ Project-URL: Issues, https://github.com/pranshu97/content-foundry/issues
10
+ Keywords: youtube,video-generation,ai-agents,llm,gemini,content-automation,text-to-speech,ffmpeg,pipeline
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Topic :: Multimedia :: Video
19
+ Classifier: Topic :: Multimedia :: Sound/Audio
20
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
21
+ Classifier: Typing :: Typed
22
+ Requires-Python: >=3.11
23
+ Description-Content-Type: text/markdown
24
+ License-File: LICENSE
25
+ Requires-Dist: pydantic>=2.7
26
+ Requires-Dist: pydantic-settings>=2.3
27
+ Requires-Dist: httpx>=0.27
28
+ Requires-Dist: beautifulsoup4>=4.12
29
+ Requires-Dist: lxml>=5.2
30
+ Requires-Dist: ddgs>=6.0
31
+ Requires-Dist: SQLAlchemy>=2.0
32
+ Requires-Dist: typer>=0.12
33
+ Requires-Dist: APScheduler>=3.10
34
+ Requires-Dist: tenacity>=8.5
35
+ Requires-Dist: structlog>=24.1
36
+ Requires-Dist: rich>=13.7
37
+ Requires-Dist: python-dateutil>=2.9
38
+ Requires-Dist: python-ulid>=2.7
39
+ Requires-Dist: Pillow>=10.4
40
+ Requires-Dist: ffmpeg-python>=0.2
41
+ Requires-Dist: pydub>=0.25
42
+ Requires-Dist: edge-tts>=6.1
43
+ Requires-Dist: anthropic>=0.39
44
+ Requires-Dist: openai>=1.50
45
+ Provides-Extra: youtube
46
+ Requires-Dist: google-api-python-client>=2.140; extra == "youtube"
47
+ Requires-Dist: google-auth-oauthlib>=1.2; extra == "youtube"
48
+ Requires-Dist: google-auth-httplib2>=0.2; extra == "youtube"
49
+ Provides-Extra: tts
50
+ Requires-Dist: elevenlabs>=1.5; extra == "tts"
51
+ Requires-Dist: piper-tts>=1.2; extra == "tts"
52
+ Provides-Extra: image
53
+ Requires-Dist: stability-sdk>=0.8; extra == "image"
54
+ Provides-Extra: video
55
+ Requires-Dist: moviepy>=1.0.3; extra == "video"
56
+ Provides-Extra: whisper
57
+ Requires-Dist: faster-whisper>=1.0; extra == "whisper"
58
+ Provides-Extra: dashboard
59
+ Requires-Dist: streamlit>=1.37; extra == "dashboard"
60
+ Provides-Extra: avatar
61
+ Requires-Dist: rembg>=2.0; extra == "avatar"
62
+ Provides-Extra: clone
63
+ Requires-Dist: chatterbox-tts>=0.1; extra == "clone"
64
+ Provides-Extra: faceid
65
+ Requires-Dist: diffusers==0.29.0; extra == "faceid"
66
+ Requires-Dist: transformers==4.44.2; extra == "faceid"
67
+ Requires-Dist: numpy<2.0.0; extra == "faceid"
68
+ Requires-Dist: safetensors==0.5.3; extra == "faceid"
69
+ Requires-Dist: accelerate>=0.28; extra == "faceid"
70
+ Requires-Dist: insightface>=0.7; extra == "faceid"
71
+ Requires-Dist: onnxruntime-gpu<1.23,>=1.19; extra == "faceid"
72
+ Requires-Dist: opencv-python-headless>=4.9; extra == "faceid"
73
+ Provides-Extra: all
74
+ Requires-Dist: elevenlabs>=1.5; extra == "all"
75
+ Requires-Dist: piper-tts>=1.2; extra == "all"
76
+ Requires-Dist: stability-sdk>=0.8; extra == "all"
77
+ Requires-Dist: moviepy>=1.0.3; extra == "all"
78
+ Requires-Dist: faster-whisper>=1.0; extra == "all"
79
+ Requires-Dist: streamlit>=1.37; extra == "all"
80
+ Requires-Dist: rembg>=2.0; extra == "all"
81
+ Requires-Dist: google-api-python-client>=2.140; extra == "all"
82
+ Requires-Dist: google-auth-oauthlib>=1.2; extra == "all"
83
+ Requires-Dist: google-auth-httplib2>=0.2; extra == "all"
84
+ Provides-Extra: dev
85
+ Requires-Dist: pytest>=8.3; extra == "dev"
86
+ Requires-Dist: pytest-mock>=3.14; extra == "dev"
87
+ Requires-Dist: pytest-cov>=5.0; extra == "dev"
88
+ Requires-Dist: respx>=0.21; extra == "dev"
89
+ Requires-Dist: ruff>=0.6; extra == "dev"
90
+ Requires-Dist: mypy>=1.11; extra == "dev"
91
+ Requires-Dist: pre-commit>=3.8; extra == "dev"
92
+ Requires-Dist: build>=1.2; extra == "dev"
93
+ Requires-Dist: twine>=5.1; extra == "dev"
94
+ Dynamic: license-file
95
+
96
+ # Content Foundry
97
+
98
+ An autonomous, fully-resumable multi-agent pipeline that turns real labor-market data into a
99
+ published (Private/Unlisted draft) YouTube video — grounded in data, gated by a
100
+ strict quality rubric, and compliant with synthetic-content disclosure by default.
101
+
102
+ **Pipeline:** Data Fetcher → Script Generator → Judge → Voiceover → Visuals → Render → Publish
103
+
104
+ > The complete engineering specification (single source of truth) lives in [`spec/`](spec/README.md).
105
+ > A high-level architecture summary is in [`TECH_REPORT.md`](TECH_REPORT.md), and the operator guide
106
+ > in [`Tutorial.md`](Tutorial.md).
107
+
108
+ ## Live channel
109
+
110
+ Watch the output live: **[youtube.com/@TheCrackedEng](https://www.youtube.com/@TheCrackedEng)**
111
+
112
+ > **Disclaimer:** This channel is 100% generated, voiced, and published autonomously by this repository.
113
+
114
+ ## Quickstart
115
+
116
+ ```bash
117
+ # 1. Create & activate a Python 3.11+ environment, then install
118
+ pip install -r requirements.txt
119
+ pip install -e . # exposes the `content-foundry` CLI
120
+
121
+ # 2. Configure
122
+ cp .env.example .env # fill in your keys (see Human_Tasks.md)
123
+
124
+ # 3. Initialise the database
125
+ python scripts/init_db.py
126
+
127
+ # 4. Smoke test (no upload, stops at the Judge)
128
+ content-foundry run --niche "tech careers" --to-stage judge
129
+
130
+ # Or produce a vertical YouTube Short instead of a long video (one switch):
131
+ content-foundry run --niche "tech careers" --idea "your topic" --format short
132
+ ```
133
+
134
+ See [`spec/23-deployment-instructions.md`](spec/23-deployment-instructions.md) for full deployment,
135
+ [`spec/17-cli-interface.md`](spec/17-cli-interface.md) for every command, and
136
+ [`Human_Tasks.md`](Human_Tasks.md) for the manual setup checklist (API keys, OAuth, Telegram bot).
137
+
138
+ ## Project layout
139
+
140
+ ```
141
+ src/content_foundry/ # the engine (models, agents, providers, pipeline, ...)
142
+ dashboard/ # Streamlit review dashboard
143
+ scripts/ # init_db, seed_demo
144
+ tests/ # unit / agent / integration / e2e (dry-run)
145
+ spec/ # the authoritative specification (25 chapters)
146
+ output/runs/<run_id>/ # per-run artifacts + media + package.md
147
+ ```
148
+
149
+ ## Cost discipline
150
+
151
+ Only **Agent 2 (Script Generator)** always calls an LLM. The Data Fetcher, most of the Judge, and
152
+ the Visuals prompt-builder are deterministic Python — free, fast, and hallucination-proof.
153
+
154
+ Cost levers (cheapest first):
155
+ - **Run the LLM locally** — `PRIMARY_PROVIDER=local` (Ollama / LM Studio / vLLM) makes generation free.
156
+ - **Free voice** — `TTS_PROVIDER=edge` (Microsoft neural, free, no key) or `piper` (fully offline), or `chatterbox` to **clone your own voice** free & locally (MIT-licensed, safe to monetize; GPU recommended). Paid: elevenlabs / openai. Voices auto-alternate male/female by run number.
157
+ - **Free visuals** — `IMAGE_PROVIDER=none` renders polished title cards; add free Pexels + Pixabay keys for real, moment-matched B-roll (a clip per narration beat), now held to a STRICT on-topic bar — a beat with no confidently-relevant clip falls back to a bespoke generated image (or a clean card when no image provider is set), never an off-topic clip.
158
+ - **Free research (default)** — `ENABLED_SOURCES=search` runs free DuckDuckGo web research on your run's topic (no key), so it works on **any** niche out of the box; the labor-market feeds (adzuna/layoffs/bls) are opt-in add-ons.
159
+ - **Free idea discovery** — `IDEA_MINING_ENABLED=true` + a free `YOUTUBE_API_KEY` mines *proven* outlier videos in your niche (views far above the channel's median) so each run builds a topic with demonstrated demand instead of a guess; best-effort, so it never blocks a run.
160
+ - **Free polish** — bundled sound effects (`SFX_ENABLED`), scene crossfades, a warm grade, and a Subscribe nudge are all local/ffmpeg (no paid services).
161
+ - **`--profile cheap`** — deterministic judge + Pillow cards (no image API) + a single revision.
162
+ - **Hard budget cap** — `ENFORCE_BUDGET_CAP=true` aborts a run once estimated month-to-date spend
163
+ reaches `MONTHLY_BUDGET_USD` (on by default; cost safety, not just an alert).
164
+ - **Resume reuses paid artifacts** — re-running a stage reuses existing voiceover/visuals instead of
165
+ paying again (use `--force` to regenerate).
166
+ - **`FAIL_FAST_SCORE`** (opt-in) — stop paying for revisions a hopeless script can't recover from.
167
+
168
+ Use `--profile quality` for publishing.
169
+
170
+ ## Testing
171
+
172
+ ```bash
173
+ pytest # unit + agent + integration + e2e dry-run, ≥85% coverage gate
174
+ ```
175
+
176
+ All tests run offline — vendors are mocked behind their protocols; no real network or API calls.
@@ -0,0 +1,81 @@
1
+ # Content Foundry
2
+
3
+ An autonomous, fully-resumable multi-agent pipeline that turns real labor-market data into a
4
+ published (Private/Unlisted draft) YouTube video — grounded in data, gated by a
5
+ strict quality rubric, and compliant with synthetic-content disclosure by default.
6
+
7
+ **Pipeline:** Data Fetcher → Script Generator → Judge → Voiceover → Visuals → Render → Publish
8
+
9
+ > The complete engineering specification (single source of truth) lives in [`spec/`](spec/README.md).
10
+ > A high-level architecture summary is in [`TECH_REPORT.md`](TECH_REPORT.md), and the operator guide
11
+ > in [`Tutorial.md`](Tutorial.md).
12
+
13
+ ## Live channel
14
+
15
+ Watch the output live: **[youtube.com/@TheCrackedEng](https://www.youtube.com/@TheCrackedEng)**
16
+
17
+ > **Disclaimer:** This channel is 100% generated, voiced, and published autonomously by this repository.
18
+
19
+ ## Quickstart
20
+
21
+ ```bash
22
+ # 1. Create & activate a Python 3.11+ environment, then install
23
+ pip install -r requirements.txt
24
+ pip install -e . # exposes the `content-foundry` CLI
25
+
26
+ # 2. Configure
27
+ cp .env.example .env # fill in your keys (see Human_Tasks.md)
28
+
29
+ # 3. Initialise the database
30
+ python scripts/init_db.py
31
+
32
+ # 4. Smoke test (no upload, stops at the Judge)
33
+ content-foundry run --niche "tech careers" --to-stage judge
34
+
35
+ # Or produce a vertical YouTube Short instead of a long video (one switch):
36
+ content-foundry run --niche "tech careers" --idea "your topic" --format short
37
+ ```
38
+
39
+ See [`spec/23-deployment-instructions.md`](spec/23-deployment-instructions.md) for full deployment,
40
+ [`spec/17-cli-interface.md`](spec/17-cli-interface.md) for every command, and
41
+ [`Human_Tasks.md`](Human_Tasks.md) for the manual setup checklist (API keys, OAuth, Telegram bot).
42
+
43
+ ## Project layout
44
+
45
+ ```
46
+ src/content_foundry/ # the engine (models, agents, providers, pipeline, ...)
47
+ dashboard/ # Streamlit review dashboard
48
+ scripts/ # init_db, seed_demo
49
+ tests/ # unit / agent / integration / e2e (dry-run)
50
+ spec/ # the authoritative specification (25 chapters)
51
+ output/runs/<run_id>/ # per-run artifacts + media + package.md
52
+ ```
53
+
54
+ ## Cost discipline
55
+
56
+ Only **Agent 2 (Script Generator)** always calls an LLM. The Data Fetcher, most of the Judge, and
57
+ the Visuals prompt-builder are deterministic Python — free, fast, and hallucination-proof.
58
+
59
+ Cost levers (cheapest first):
60
+ - **Run the LLM locally** — `PRIMARY_PROVIDER=local` (Ollama / LM Studio / vLLM) makes generation free.
61
+ - **Free voice** — `TTS_PROVIDER=edge` (Microsoft neural, free, no key) or `piper` (fully offline), or `chatterbox` to **clone your own voice** free & locally (MIT-licensed, safe to monetize; GPU recommended). Paid: elevenlabs / openai. Voices auto-alternate male/female by run number.
62
+ - **Free visuals** — `IMAGE_PROVIDER=none` renders polished title cards; add free Pexels + Pixabay keys for real, moment-matched B-roll (a clip per narration beat), now held to a STRICT on-topic bar — a beat with no confidently-relevant clip falls back to a bespoke generated image (or a clean card when no image provider is set), never an off-topic clip.
63
+ - **Free research (default)** — `ENABLED_SOURCES=search` runs free DuckDuckGo web research on your run's topic (no key), so it works on **any** niche out of the box; the labor-market feeds (adzuna/layoffs/bls) are opt-in add-ons.
64
+ - **Free idea discovery** — `IDEA_MINING_ENABLED=true` + a free `YOUTUBE_API_KEY` mines *proven* outlier videos in your niche (views far above the channel's median) so each run builds a topic with demonstrated demand instead of a guess; best-effort, so it never blocks a run.
65
+ - **Free polish** — bundled sound effects (`SFX_ENABLED`), scene crossfades, a warm grade, and a Subscribe nudge are all local/ffmpeg (no paid services).
66
+ - **`--profile cheap`** — deterministic judge + Pillow cards (no image API) + a single revision.
67
+ - **Hard budget cap** — `ENFORCE_BUDGET_CAP=true` aborts a run once estimated month-to-date spend
68
+ reaches `MONTHLY_BUDGET_USD` (on by default; cost safety, not just an alert).
69
+ - **Resume reuses paid artifacts** — re-running a stage reuses existing voiceover/visuals instead of
70
+ paying again (use `--force` to regenerate).
71
+ - **`FAIL_FAST_SCORE`** (opt-in) — stop paying for revisions a hopeless script can't recover from.
72
+
73
+ Use `--profile quality` for publishing.
74
+
75
+ ## Testing
76
+
77
+ ```bash
78
+ pytest # unit + agent + integration + e2e dry-run, ≥85% coverage gate
79
+ ```
80
+
81
+ All tests run offline — vendors are mocked behind their protocols; no real network or API calls.
@@ -0,0 +1,158 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "content-foundry"
7
+ dynamic = ["version"]
8
+ description = "A resumable multi-agent pipeline that researches, writes, judges, narrates, and renders short-form YouTube videos for any niche."
9
+ readme = "README.md"
10
+ requires-python = ">=3.11"
11
+ license = { text = "MIT" }
12
+ authors = [{ name = "Pranshu Mishra", email = "pranshumshr.04@gmail.com" }]
13
+ keywords = [
14
+ "youtube", "video-generation", "ai-agents", "llm", "gemini",
15
+ "content-automation", "text-to-speech", "ffmpeg", "pipeline",
16
+ ]
17
+ classifiers = [
18
+ "Development Status :: 4 - Beta",
19
+ "Intended Audience :: Developers",
20
+ "License :: OSI Approved :: MIT License",
21
+ "Operating System :: OS Independent",
22
+ "Programming Language :: Python :: 3",
23
+ "Programming Language :: Python :: 3.11",
24
+ "Programming Language :: Python :: 3.12",
25
+ "Topic :: Multimedia :: Video",
26
+ "Topic :: Multimedia :: Sound/Audio",
27
+ "Topic :: Scientific/Engineering :: Artificial Intelligence",
28
+ "Typing :: Typed",
29
+ ]
30
+
31
+ # Lean core — everything the DEFAULT pipeline needs. Optional integrations live in the extras below
32
+ # (install on demand, e.g. `pip install content-foundry[all]`); every vendor SDK is imported lazily.
33
+ dependencies = [
34
+ "pydantic>=2.7",
35
+ "pydantic-settings>=2.3",
36
+ "httpx>=0.27",
37
+ "beautifulsoup4>=4.12",
38
+ "lxml>=5.2",
39
+ "ddgs>=6.0",
40
+ "SQLAlchemy>=2.0",
41
+ "typer>=0.12",
42
+ "APScheduler>=3.10",
43
+ "tenacity>=8.5",
44
+ "structlog>=24.1",
45
+ "rich>=13.7",
46
+ "python-dateutil>=2.9",
47
+ "python-ulid>=2.7",
48
+ "Pillow>=10.4",
49
+ "ffmpeg-python>=0.2",
50
+ "pydub>=0.25",
51
+ "edge-tts>=6.1",
52
+ "anthropic>=0.39",
53
+ "openai>=1.50",
54
+ ]
55
+
56
+ [project.optional-dependencies]
57
+ youtube = ["google-api-python-client>=2.140", "google-auth-oauthlib>=1.2", "google-auth-httplib2>=0.2"]
58
+ tts = ["elevenlabs>=1.5", "piper-tts>=1.2"]
59
+ image = ["stability-sdk>=0.8"]
60
+ video = ["moviepy>=1.0.3"]
61
+ whisper = ["faster-whisper>=1.0"]
62
+ dashboard = ["streamlit>=1.37"]
63
+ avatar = ["rembg>=2.0"]
64
+ clone = ["chatterbox-tts>=0.1"]
65
+ faceid = [
66
+ # Pinned to coexist with chatterbox-tts, which needs diffusers==0.29.0: a newer transformers or
67
+ # numpy>=2 breaks diffusers 0.29's Stable Diffusion import. See providers/faceid.py.
68
+ "diffusers==0.29.0", "transformers==4.44.2", "numpy<2.0.0", "safetensors==0.5.3",
69
+ # onnxruntime-gpu pinned to the CUDA 12 line (matches torch cu124); 1.23+ needs CUDA 13 libs this env lacks.
70
+ "accelerate>=0.28", "insightface>=0.7", "onnxruntime-gpu>=1.19,<1.23", "opencv-python-headless>=4.9",
71
+ ]
72
+ all = [
73
+ "elevenlabs>=1.5", "piper-tts>=1.2", "stability-sdk>=0.8", "moviepy>=1.0.3",
74
+ "faster-whisper>=1.0", "streamlit>=1.37", "rembg>=2.0",
75
+ "google-api-python-client>=2.140", "google-auth-oauthlib>=1.2", "google-auth-httplib2>=0.2",
76
+ ]
77
+ dev = [
78
+ "pytest>=8.3",
79
+ "pytest-mock>=3.14",
80
+ "pytest-cov>=5.0",
81
+ "respx>=0.21",
82
+ "ruff>=0.6",
83
+ "mypy>=1.11",
84
+ "pre-commit>=3.8",
85
+ "build>=1.2",
86
+ "twine>=5.1",
87
+ ]
88
+
89
+ [project.urls]
90
+ Homepage = "https://github.com/pranshu97/content-foundry"
91
+ Repository = "https://github.com/pranshu97/content-foundry"
92
+ Issues = "https://github.com/pranshu97/content-foundry/issues"
93
+
94
+ [project.scripts]
95
+ content-foundry = "content_foundry.cli:app"
96
+
97
+ [tool.setuptools.dynamic]
98
+ version = { attr = "content_foundry.__version__" }
99
+
100
+ [tool.setuptools.packages.find]
101
+ where = ["src"]
102
+
103
+ [tool.setuptools.package-data]
104
+ content_foundry = ["prompts/*.txt", "data/sounds/*", "py.typed"]
105
+
106
+ # ---------------------------------------------------------------------------
107
+ # Tooling
108
+ # ---------------------------------------------------------------------------
109
+ [tool.ruff]
110
+ line-length = 100
111
+ target-version = "py311"
112
+ src = ["src", "tests"]
113
+
114
+ [tool.ruff.lint]
115
+ select = ["E", "F", "I", "UP", "B", "C4", "SIM"]
116
+ ignore = ["E501", "UP042"]
117
+
118
+ [tool.mypy]
119
+ python_version = "3.11"
120
+ warn_unused_ignores = true
121
+ ignore_missing_imports = true
122
+ plugins = ["pydantic.mypy"]
123
+ packages = ["content_foundry"]
124
+ mypy_path = "src"
125
+
126
+ [tool.pytest.ini_options]
127
+ minversion = "8.0"
128
+ addopts = "-q --cov=content_foundry --cov-report=term-missing"
129
+ testpaths = ["tests"]
130
+ pythonpath = ["src"]
131
+ filterwarnings = ["ignore::DeprecationWarning"]
132
+
133
+ [tool.coverage.run]
134
+ source = ["content_foundry"]
135
+ branch = true
136
+ omit = [
137
+ # Thin vendor adapter glue (network/SDK side effects) — excluded per Ch. 22.5.
138
+ "*/providers/anthropic_provider.py",
139
+ "*/providers/openai_provider.py",
140
+ "*/providers/tts.py",
141
+ "*/providers/image.py",
142
+ "*/providers/broll.py",
143
+ "*/providers/render_backend.py",
144
+ "*/providers/youtube.py",
145
+ "*/notifications/telegram.py",
146
+ "*/scheduler.py",
147
+ "*/cli.py",
148
+ ]
149
+
150
+ [tool.coverage.report]
151
+ fail_under = 85
152
+ show_missing = true
153
+ exclude_lines = [
154
+ "pragma: no cover",
155
+ "raise NotImplementedError",
156
+ "if TYPE_CHECKING:",
157
+ "\\.\\.\\.",
158
+ ]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,4 @@
1
+ """Content Foundry — a resumable multi-agent pipeline that researches, writes, judges, narrates, and
2
+ renders short-form YouTube videos for any niche."""
3
+
4
+ __version__ = "1.0.0"
@@ -0,0 +1,36 @@
1
+ """The seven agents (each owns exactly one transformation; no agent imports another)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from .brainstorm import Brainstormer
6
+ from .broll_director import BrollDirector
7
+ from .data_fetcher import DataFetcher
8
+ from .idea_miner import IdeaMiner
9
+ from .instruction_planner import InstructionPlanner
10
+ from .judge import Judge
11
+ from .publisher import Publisher
12
+ from .renderer import Renderer
13
+ from .research import Researcher
14
+ from .scene_image_director import SceneImageDirector
15
+ from .script_generator import ScriptGenerator
16
+ from .thumbnail_director import ThumbnailDirector
17
+ from .visuals import Visuals, build_image_prompt
18
+ from .voiceover import Voiceover
19
+
20
+ __all__ = [
21
+ "DataFetcher",
22
+ "Brainstormer",
23
+ "IdeaMiner",
24
+ "InstructionPlanner",
25
+ "Researcher",
26
+ "BrollDirector",
27
+ "ScriptGenerator",
28
+ "ThumbnailDirector",
29
+ "SceneImageDirector",
30
+ "Judge",
31
+ "Voiceover",
32
+ "Visuals",
33
+ "build_image_prompt",
34
+ "Renderer",
35
+ "Publisher",
36
+ ]
@@ -0,0 +1,136 @@
1
+ """Agent 0 — Brainstormer. Proposes several fresh, specific, helpful video ideas from the brief so
2
+ runs don't collapse onto the same topic. LLM-driven with a deterministic content-angle fallback."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import json
7
+
8
+ from ..errors import LLMError
9
+ from ..logging import get_logger
10
+ from ..models import DataBrief
11
+ from ..prompts import load_prompt, render_prompt
12
+ from ..providers.base import LLMProvider, extract_json
13
+ from ..providers.tiering import TaskTier, select_model
14
+
15
+
16
+ class Brainstormer:
17
+ def __init__(self, settings, llm_provider: LLMProvider):
18
+ self._settings = settings
19
+ self._llm = llm_provider
20
+ self._log = get_logger(component="brainstorm")
21
+
22
+ def propose(
23
+ self, brief: DataBrief, *, recent_ideas: list[str] | None = None, count: int = 5,
24
+ focus: str = "",
25
+ ) -> list[str]:
26
+ """Propose ``count`` distinct, specific video ideas. ``focus`` (e.g. the user's --idea) steers
27
+ every idea. The fetched data is used only as INSPIRATION / supporting stats, never the whole
28
+ basis. Falls back to deterministic content angles when the LLM is unavailable."""
29
+ recent = [r for r in (recent_ideas or []) if r]
30
+ fallback = self._fallback(brief, recent, focus, count)
31
+ try:
32
+ system = render_prompt(
33
+ load_prompt("brainstorm.system"),
34
+ niche=brief.niche,
35
+ count=str(count),
36
+ focus_line=(
37
+ f'The viewer specifically asked for: "{focus}". EVERY idea MUST be a concrete, '
38
+ "specific angle that delivers on this." if focus else ""
39
+ ),
40
+ facts_json=json.dumps([kf.statement for kf in brief.key_facts], ensure_ascii=False),
41
+ avoid_json=json.dumps(recent[:8], ensure_ascii=False),
42
+ )
43
+ resp = self._llm.complete(
44
+ "Return ONLY the JSON array now.",
45
+ system=system,
46
+ temperature=max(self._settings.llm_temperature, 0.8),
47
+ # A generous cap: reasoning models (e.g. Gemini flash) spend part of the budget
48
+ # THINKING, and a tight cap (700) left no room for the JSON -> empty/unusable reply.
49
+ max_tokens=self._settings.llm_max_tokens,
50
+ model=select_model(
51
+ self._settings, TaskTier.HEAVY, fallback=self._settings.generator_model
52
+ ),
53
+ )
54
+ ideas = _parse_ideas(resp.text)
55
+ if ideas:
56
+ self._log.info("brainstormed_ideas", count=len(ideas))
57
+ return ideas[:count]
58
+ except (json.JSONDecodeError, LLMError, KeyError, ValueError, AttributeError, TypeError) as exc:
59
+ self._log.warning("brainstorm_fallback", error=str(exc))
60
+ return fallback
61
+
62
+ def run(self, brief: DataBrief, *, recent_ideas: list[str] | None = None) -> str:
63
+ """Back-compat single-idea helper."""
64
+ ideas = self.propose(brief, recent_ideas=recent_ideas, count=1)
65
+ return ideas[0] if ideas else ""
66
+
67
+ def _fallback(self, brief: DataBrief, recent: list[str], focus: str, count: int) -> list[str]:
68
+ """Deterministic: clean focus-based angles (or the brief's content angles when there is no
69
+ focus), skipping recently-made ones."""
70
+ pool: list[str] = []
71
+ if focus:
72
+ f = focus.strip().rstrip(".")
73
+ low = (f[:1].lower() + f[1:]) if f else f
74
+ pool += [
75
+ f"{f}: a practical step-by-step guide",
76
+ f"{f}: the mistakes that keep you stuck",
77
+ f"{f}: what actually works, backed by the data",
78
+ f"The truth about {low}",
79
+ f"{f}: what the {brief.niche} numbers really say",
80
+ f"{f} in 30 days: a realistic plan",
81
+ f"{f}: the first moves to make right now",
82
+ ]
83
+ pool += [a.hook for a in brief.content_angles]
84
+ if not pool:
85
+ pool = [f"A specific, data-backed {brief.niche} explainer that solves one real problem"]
86
+ recent_l = " ".join(recent).lower()
87
+ fresh = [h for h in _dedup(pool) if h[:25].lower() not in recent_l]
88
+ return (fresh or _dedup(pool))[:count]
89
+
90
+
91
+ def _dedup(items) -> list[str]:
92
+ """Order-preserving de-dup + trim; coerces non-strings to str."""
93
+ seen: set[str] = set()
94
+ out: list[str] = []
95
+ for item in items:
96
+ text = (item if isinstance(item, str) else str(item)).strip()
97
+ if text and text.lower() not in seen:
98
+ seen.add(text.lower())
99
+ out.append(text)
100
+ return out
101
+
102
+
103
+ def _slice_array(text: str) -> str:
104
+ """Slice the first ``[`` to the last ``]`` (recovers a JSON array wrapped in prose)."""
105
+ start, end = text.find("["), text.rfind("]")
106
+ return text[start : end + 1] if start != -1 and end > start else ""
107
+
108
+
109
+ def _parse_ideas(text: str) -> list[str]:
110
+ """Robustly pull the list of idea strings from an LLM reply. Handles a bare JSON array, an object
111
+ ``{"ideas": [...]}``, an array of objects, or an array wrapped in prose. NOTE: a plain
112
+ ``extract_json`` MANGLES a top-level array (it slices first ``{`` to last ``}``), so we try a
113
+ direct parse and an array-slice too."""
114
+ raw = (text or "").strip()
115
+ data = None
116
+ for candidate in (raw, extract_json(raw), _slice_array(raw)):
117
+ if not candidate:
118
+ continue
119
+ try:
120
+ data = json.loads(candidate)
121
+ break
122
+ except (json.JSONDecodeError, ValueError):
123
+ continue
124
+ if isinstance(data, dict):
125
+ data = data.get("ideas") or data.get("items") or data.get("titles") or []
126
+ if not isinstance(data, list):
127
+ return []
128
+ out: list[str] = []
129
+ for item in data:
130
+ if isinstance(item, str):
131
+ out.append(item)
132
+ elif isinstance(item, dict): # array of objects -> use the most title-like field
133
+ val = item.get("title") or item.get("idea") or item.get("angle") or item.get("text")
134
+ if isinstance(val, str):
135
+ out.append(val)
136
+ return _dedup(out)