Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ dist/
.env
staging_docs/
output/
topics/*
!topics/example.md
!topics/.gitkeep
.next/
docs/out/
docs/src/data/
Expand Down
22 changes: 22 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,7 @@ pnpm start "your topic" --archetype anime_illustration --provider openai
| `--tts-provider <name>` | TTS provider (`elevenlabs`, `inworld`, `kokoro`, `gemini-tts`, `openai-tts`) | `elevenlabs` |
| `--music-provider <name>` | Music provider (`bundled`, `lyria`) | `bundled` |
| `--video-provider <name>` | Video provider (`gemini`, `fal`) | auto-detect |
| `-c, --context <file>` | Path to a context file with extra direction (see [Context files](#context-files)) | none |
| `--archetype <name>` | Override visual archetype | LLM chooses |
| `--platform <name>` | Target platform (`youtube`, `tiktok`, `instagram`) | `youtube` |
| `--dry-run` | Output DirectorScore JSON without generating assets | off |
Expand All @@ -178,6 +179,27 @@ pnpm start "your topic" --archetype anime_illustration --provider openai
| `--kokoro-voice <voice>` | Kokoro voice preset | `af_heart` |
| `-y, --yes` | Auto-confirm cost estimation (Docker/CI) | off |

### Context files

Want more control over the output? Write a context file with your angle, key points, tone, and constraints, then pass it with `--context`:

```bash
pnpm start "the apollo 13 disaster" --context topics/apollo-brief.md
```

The `topics/` folder is gitignored (your briefs stay private). See `topics/example.md` for a template.

Context is injected into the research, creative director, and critic stages so the entire pipeline follows your direction. You can use markdown or plain text — there's no required format.

Via the API, pass `context` as a string in the request body:

```json
{
"topic": "the apollo 13 disaster",
"context": "Focus on the human side, not technical details..."
}
```

## Cost transparency

Before spending any money, the pipeline shows a detailed cost breakdown and asks for confirmation:
Expand Down
4 changes: 2 additions & 2 deletions src/agents/creative-director.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export async function generateDirectorScore(
llm: LLMProvider,
topic: string,
researchContext: ResearchResult,
options?: { archetype?: string; pacing?: string; videoEnabled?: boolean },
options?: { archetype?: string; pacing?: string; videoEnabled?: boolean; context?: string },
): Promise<DirectorScoreOutput> {
let systemPrompt = buildDefaultPrompt();

Expand Down Expand Up @@ -89,7 +89,7 @@ Use ${visualTypes}.${videoGuidance}
CRITICAL RULE: Never use the same visual_type more than 2 times in a row. With more scenes, plan your visual_type sequence BEFORE writing scenes to ensure variety.
Every scene MUST have a script_line (the voiceover text).
The first scene should be a strong hook.
If over budget, cut a scene rather than cramming.`;
If over budget, cut a scene rather than cramming.${options?.context ? `\n\nCreator's additional context and direction (follow this closely):\n${options.context}` : ""}`;

const maxRetries = 3;
let lastError: Error | null = null;
Expand Down
3 changes: 2 additions & 1 deletion src/agents/critic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export async function evaluate(
score: DirectorScore,
topic: string,
pacingOverride?: string,
context?: string,
): Promise<CritiqueOutput> {
let systemPrompt =
"You are a video quality critic. Evaluate the DirectorScore for hook strength, visual variety, pacing, script quality, and overall coherence. Score 1-10. If below 7, provide specific revision instructions targeting the weakest scene.";
Expand Down Expand Up @@ -75,7 +76,7 @@ Evaluate pacing against these tier-specific thresholds, NOT a fixed "5-7 scenes"
DirectorScore:
${JSON.stringify(score, null, 2)}

Evaluate this video plan. Score it 1-10. If it scores below 7, identify the weakest scene and provide specific revision instructions.`;
Evaluate this video plan. Score it 1-10. If it scores below 7, identify the weakest scene and provide specific revision instructions.${context ? `\n\nCreator's additional context and direction (evaluate whether the plan follows this):\n${context}` : ""}`;

const result = await llm.generate({
systemPrompt,
Expand Down
9 changes: 7 additions & 2 deletions src/agents/research.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export interface ResearchOutput {
usage: LLMUsage;
}

export async function research(llm: LLMProvider, topic: string): Promise<ResearchOutput> {
export async function research(llm: LLMProvider, topic: string, context?: string): Promise<ResearchOutput> {
let systemPrompt =
"You are a research assistant. Given a topic, search the web for current information and produce a structured research summary with key facts, mood/tone, and sources.";

Expand All @@ -28,9 +28,14 @@ export async function research(llm: LLMProvider, topic: string): Promise<Researc
// Use default prompt if file doesn't exist
}

let userMessage = `Research this topic for a short-form video script: ${topic}`;
if (context) {
userMessage += `\n\nAdditional context and direction from the creator:\n${context}`;
}

const result = await llm.generate({
systemPrompt,
userMessage: `Research this topic for a short-form video script: ${topic}`,
userMessage,
schema: ResearchResult,
enableWebSearch: true,
});
Expand Down
3 changes: 3 additions & 0 deletions src/cli/args.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ const { version } = require("../../package.json") as { version: string };

export interface CLIOptions {
topic: string;
context?: string;
provider: LLMProviderKey;
imageProvider: ImageProviderKey;
ttsProvider: TTSProviderKey;
Expand Down Expand Up @@ -54,6 +55,7 @@ export function parseArgs(): CLIOptions {
.default("elevenlabs"),
)
.option("--kokoro-voice <voice>", "Kokoro voice preset (e.g. af_heart, bf_emma, am_fenrir)", "af_heart")
.option("-c, --context <file>", "Path to a context file with additional topic details (e.g. topics/my-brief.md)")
.option("-a, --archetype <archetype>", "Visual archetype override")
.addOption(
new Option("--pacing <tier>", "Pacing tier override (overrides archetype default)")
Expand Down Expand Up @@ -121,6 +123,7 @@ export function parseArgs(): CLIOptions {

return {
topic,
context: opts["context"] as string | undefined,
provider: opts["provider"] as LLMProviderKey,
imageProvider: opts["imageProvider"] as ImageProviderKey,
ttsProvider: opts["ttsProvider"] as TTSProviderKey,
Expand Down
17 changes: 17 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#!/usr/bin/env node

import * as fs from "node:fs";
import { parseArgs } from "./cli/args.js";
import { validateEnv } from "./cli/validate-env.js";
import { createCliCallbacks, runPipeline } from "./pipeline/orchestrator.js";
Expand Down Expand Up @@ -28,6 +29,21 @@ async function main(): Promise<void> {
kokoroVoice: opts.kokoroVoice,
});

// Read context file if provided
let context: string | undefined;
if (opts.context) {
try {
context = fs.readFileSync(opts.context, "utf-8").trim();
if (!context) {
console.warn(`Warning: Context file "${opts.context}" is empty, ignoring.`);
context = undefined;
}
} catch (err) {
console.error(`Error reading context file "${opts.context}":`, err instanceof Error ? err.message : String(err));
process.exit(1);
}
}

// Create CLI callbacks for terminal progress display
const { callbacks, progress } = createCliCallbacks(opts.yes);

Expand All @@ -40,6 +56,7 @@ async function main(): Promise<void> {
const result = await runPipeline(
{
topic: opts.topic,
context,
llm,
tts,
ttsProvider: opts.ttsProvider,
Expand Down
5 changes: 3 additions & 2 deletions src/pipeline/orchestrator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -368,7 +368,7 @@ function buildPipelineWorkflow(
cb.onStageStart?.("research");
const start = Date.now();
try {
const output = await research(opts.llm, inputData.topic);
const output = await research(opts.llm, inputData.topic, opts.context);
llmUsages.push(output.usage);
const dur = (Date.now() - start) / 1000;
cb.onStageComplete?.("research", `${output.data.key_facts.length} facts`, dur);
Expand Down Expand Up @@ -414,6 +414,7 @@ function buildPipelineWorkflow(
archetype: opts.archetype,
pacing: opts.pacing,
videoEnabled,
context: opts.context,
});
// Store on shared context via closure
directorResult.score = cdOutput.data;
Expand Down Expand Up @@ -729,7 +730,7 @@ function buildPipelineWorkflow(
cb.onStageStart?.("critic");
const start = Date.now();
try {
const critiqueOutput = await evaluate(opts.llm, score, opts.topic, opts.pacing);
const critiqueOutput = await evaluate(opts.llm, score, opts.topic, opts.pacing, opts.context);
const critique = critiqueOutput.data;
llmUsages.push(critiqueOutput.usage);
const dur = (Date.now() - start) / 1000;
Expand Down
1 change: 1 addition & 0 deletions src/pipeline/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ export interface PipelineCallbacks {

export interface PipelineOptions {
topic: string;
context?: string;
llm: LLMProvider;
tts: TTSProvider;
ttsProvider: TTSProviderKey;
Expand Down
9 changes: 8 additions & 1 deletion src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,7 @@ app.get("/api/v1/providers", async () => ({
// --- Job creation ---
interface CreateJobBody {
topic: string;
context?: string;
archetype?: string;
pacing?: string;
platform?: string;
Expand All @@ -176,7 +177,7 @@ interface CreateJobBody {
}

app.post<{ Body: CreateJobBody }>("/api/v1/jobs", async (request, reply) => {
const { topic, archetype, pacing, platform, dryRun, noMusic, noVideo, providers, keys } = request.body ?? {};
const { topic, context, archetype, pacing, platform, dryRun, noMusic, noVideo, providers, keys } = request.body ?? {};

if (!topic || typeof topic !== "string" || topic.trim().length === 0) {
return reply.status(400).send({ error: "topic is required" });
Expand Down Expand Up @@ -210,8 +211,14 @@ app.post<{ Body: CreateJobBody }>("/api/v1/jobs", async (request, reply) => {
.send({ error: `Unknown platform: ${platform}. Available: ${validPlatforms.join(", ")}` });
}

// Validate context if provided
if (context !== undefined && (typeof context !== "string" || context.trim().length > 5000)) {
return reply.status(400).send({ error: "context must be a string of 5000 characters or fewer" });
}

const job = await queue.add("render", {
topic: topic.trim(),
context: context?.trim() || undefined,
archetype,
pacing,
platform: platform ?? "youtube",
Expand Down
4 changes: 3 additions & 1 deletion src/worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ try {

interface JobData {
topic: string;
context?: string;
archetype?: string;
pacing?: string;
platform: string;
Expand Down Expand Up @@ -84,7 +85,7 @@ function writeMeta(jobDir: string, meta: JobMeta) {
const worker = new Worker<JobData>(
"openreels",
async (job: Job<JobData>) => {
const { topic, archetype, pacing, platform, dryRun, noMusic, noVideo, providers, keys } = job.data;
const { topic, context, archetype, pacing, platform, dryRun, noMusic, noVideo, providers, keys } = job.data;
const jobDir = path.join(JOBS_DIR, job.id!);
fs.mkdirSync(jobDir, { recursive: true });

Expand Down Expand Up @@ -221,6 +222,7 @@ const worker = new Worker<JobData>(
const result = await runPipeline(
{
topic,
context,
llm: providerInstances.llm,
tts: providerInstances.tts,
ttsProvider: providers.tts as TTSProviderKey,
Expand Down
Empty file added topics/.gitkeep
Empty file.
39 changes: 39 additions & 0 deletions topics/example.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# Example Context File

Place your context files here to give OpenReels more direction when generating videos.

## How to use

Create a markdown (or plain text) file in this folder describing what you want, then reference it with the `--context` flag:

```bash
pnpm start "the apollo 13 disaster" --context topics/my-apollo-brief.md
```

## What to include

- **Angle / thesis**: What specific perspective should the video take?
- **Key points**: Must-include facts, quotes, or data points.
- **Tone**: Serious, humorous, dramatic, educational, etc.
- **Target audience**: Who is this for?
- **Call to action**: What should viewers do after watching?
- **Things to avoid**: Topics, claims, or imagery you don't want.

## Example

```markdown
# Apollo 13 — The Human Side

Focus on the emotional experience of the astronauts and mission control,
not the technical details. Highlight the moment Jim Lovell saw the oxygen
venting and knew they were in trouble.

Tone: Tense, then hopeful. End on the triumphant splashdown.

Must include:
- "Houston, we've had a problem" (use the correct quote, not the movie version)
- The CO2 scrubber improvisation
- The 4-minute blackout during reentry

Avoid: Conspiracy theories, movie dramatizations presented as fact.
```
Loading