Merge commit '3896991bdb91210d1574044bc5a321e269b4698e'

This commit is contained in:
Alexa Amundson
2025-11-28 22:56:51 -06:00
22 changed files with 453 additions and 0 deletions

16
.eslintrc.json Normal file
View File

@@ -0,0 +1,16 @@
{
"env": {
"es2022": true,
"node": true
},
"extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended", "prettier"],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": "latest",
"sourceType": "module"
},
"plugins": ["@typescript-eslint"],
"rules": {
"@typescript-eslint/explicit-function-return-type": "off"
}
}

View File

@@ -38,3 +38,30 @@ jobs:
- name: Test - name: Test
run: npm test --if-present run: npm test --if-present
name: ci
on:
push:
branches: [work]
pull_request:
jobs:
lint-and-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: 8
- uses: actions/setup-node@v4
with:
node-version: 20
cache: pnpm
- run: pnpm install --frozen-lockfile=true
- run: pnpm lint
- name: Validate JSON
run: jq empty $(find . -name "*.json")
- name: Validate YAML
run: |
pnpm add -g yamllint
yamllint prompts

8
.gitignore vendored Normal file
View File

@@ -0,0 +1,8 @@
node_modules/
package-lock.json
*.log
.DS_Store
dist/
build/
.env
.env.local

2
.prettierignore Normal file
View File

@@ -0,0 +1,2 @@
# Handlebars templates should not be formatted by prettier
workflows/*.json.hbs

8
.prettierrc.cjs Normal file
View File

@@ -0,0 +1,8 @@
// Note: The 90-character line limit mentioned in prompts/video-intro.yml applies only to subtitle text.
// The printWidth setting below is for code formatting and does not affect subtitle text.
module.exports = {
singleQuote: true,
semi: true,
trailingComma: 'all',
printWidth: 100,
};

View File

@@ -36,3 +36,42 @@ Creator-focused pack for BlackRoad OS with content planning, longform scripting,
## Limitations ## Limitations
- Channel adapters (YouTube, TikTok, etc.) are not wired; outputs are adapter-ready stubs. - Channel adapters (YouTube, TikTok, etc.) are not wired; outputs are adapter-ready stubs.
- Analytics hooks are placeholders; performance insights should be injected via API or data warehouse taps. - Analytics hooks are placeholders; performance insights should be injected via API or data warehouse taps.
# Blackroad OS · Creator-Studio Pack (Gen-0)
CreatorPack-Gen-0 scaffolds a prompt-first toolkit for designers, writers, and video makers.
It ships with curated prompt presets, tiny agent helpers, and workflow templates that can be
rendered with Handlebars.
## Quickstart
```bash
pnpm i
pnpm br-create list
pnpm br-create run brand-kit
```
Set environment variables using `creator-studio.env.example` and export them before running
remote APIs.
## Layout
- `/prompts` — YAML presets with front-matter metadata for creative tasks.
- `/agents` — TypeScript and Python helpers for prompt generation, media remixing, and Canva
rendering (stubs).
- `/workflows` — Handlebars JSON templates for Canva and FFmpeg jobs.
- `/lib` — Shared Handlebars renderer and zod schemas.
- `/src` — CLI entry for `br-create` commands.
- `/scripts` — Build-time helpers such as beacon injection.
## Commands
- `pnpm br-create list` — enumerate prompts and workflows.
- `pnpm br-create run <prompt>` — send a prompt preset to the configured agent.
- `pnpm br-create render <workflow>` — fill a workflow JSON template.
- `pnpm br-create render-canva <workflow>` — fill a Canva workflow JSON template.
- `pnpm lint` — run ESLint + Prettier checks.
## Roadmap
- TODO(creator-pack-next): Blender pipeline for 3D packshots.
- TODO(creator-pack-next): Audio mastering agent for podcast polish.

26
agents/generate_prompt.ts Normal file
View File

@@ -0,0 +1,26 @@
import fs from 'node:fs';
import path from 'node:path';
import yaml from 'js-yaml';
import { promptPresetSchema, PromptPreset } from '../lib/schema.js';
export const loadPromptPreset = (slug: string): PromptPreset => {
const filePath = path.join(process.cwd(), 'prompts', `${slug}.yml`);
const file = fs.readFileSync(filePath, 'utf-8');
const data = yaml.load(file);
const parsed = promptPresetSchema.parse(data);
return parsed;
};
export const renderPrompt = (preset: PromptPreset, agentName: string): string => {
const steps = preset.steps.map((step, index) => `${index + 1}. ${step.text}`).join('\n');
const header = `# ${preset.title}\nmodel: ${preset.model}\nagent: ${agentName}`;
const notes = preset.notes ? `\n\nNotes:\n${preset.notes}` : '';
return `${header}\n\n${preset.description}\n\nSteps:\n${steps}${notes}`;
};
export const dispatchPrompt = async (preset: PromptPreset, agentName: string): Promise<string> => {
const promptText = renderPrompt(preset, agentName);
// Placeholder for SDK call to OpenAI or internal agent.
// TODO(creator-pack-next): Stream completions to file for downstream workflows.
return Promise.resolve(`Sent to ${agentName}:\n${promptText}`);
};

30
agents/remix_media.py Normal file
View File

@@ -0,0 +1,30 @@
"""Tiny ffmpeg wrapper used for text-only parameter composition."""
from __future__ import annotations
from dataclasses import dataclass
from pathlib import Path
from typing import List
@dataclass
class RemixJob:
"""Describes a simplified ffmpeg remix operation."""
input_path: Path
output_path: Path
filters: List[str]
bitrate: str = "6M"
def build_command(self) -> str:
filtergraph = ",".join(self.filters)
return f"ffmpeg -i {self.input_path} -b:v {self.bitrate} -vf \"{filtergraph}\" {self.output_path}"
def preview_job(job: RemixJob) -> str:
"""Return the ffmpeg command without executing it."""
return job.build_command()
# TODO(creator-pack-next): Execute ffmpeg with subprocess and stream logs.

22
agents/render_canva.ts Normal file
View File

@@ -0,0 +1,22 @@
import { readFileSync } from 'node:fs';
import path from 'node:path';
import { renderTemplate } from '../lib/template.js';
interface CanvaJobContext {
brand: string;
assets: string[];
agent: string;
}
export const renderCanvaBatch = (templateName: string, context: CanvaJobContext): string => {
const filePath = path.join(process.cwd(), 'workflows', `${templateName}.json.hbs`);
const source = readFileSync(filePath, 'utf-8');
const rendered = renderTemplate(source, context);
return rendered;
};
export const sendToCanva = async (payload: string): Promise<string> => {
const token = process.env.CANVA_API_TOKEN || 'unset-token';
// Placeholder for Canva API call.
return Promise.resolve(`POST /canva with token=${token}\n${payload}`);
};

View File

@@ -0,0 +1,8 @@
# Canva API token
CANVA_API_TOKEN=your-canva-token
# OpenAI API key used by generate_prompt.ts
OPENAI_API_KEY=your-openai-key
# Default agent name for CLI operations
CREATOR_AGENT=lucidia

24
lib/schema.ts Normal file
View File

@@ -0,0 +1,24 @@
import { z } from 'zod';
export const promptStepSchema = z.object({
text: z.string(),
});
export const promptPresetSchema = z.object({
title: z.string(),
description: z.string(),
model: z.string(),
tags: z.array(z.string()),
steps: z.array(promptStepSchema),
notes: z.string().optional(),
});
export const workflowTemplateSchema = z.object({
id: z.string(),
description: z.string(),
engine: z.enum(['canva', 'ffmpeg']),
template: z.unknown(),
});
export type PromptPreset = z.infer<typeof promptPresetSchema>;
export type WorkflowTemplate = z.infer<typeof workflowTemplateSchema>;

9
lib/template.ts Normal file
View File

@@ -0,0 +1,9 @@
import Handlebars from 'handlebars';
export const renderTemplate = <T extends object>(source: string, context: T): string => {
// WARNING: 'noEscape: true' disables HTML escaping in Handlebars templates.
// This is intentional for non-HTML contexts (e.g., JSON), but can lead to injection vulnerabilities
// if used with untrusted user input. Ensure that 'context' is trusted and sanitized before use.
const template = Handlebars.compile(source, { noEscape: true });
return template(context);
};

View File

@@ -10,6 +10,27 @@
"@types/mocha": "^10.0.6", "@types/mocha": "^10.0.6",
"@types/node": "^22.7.4", "@types/node": "^22.7.4",
"mocha": "^10.7.3", "mocha": "^10.7.3",
"private": true,
"type": "module",
"scripts": {
"br-create": "ts-node src/cli.ts",
"lint": "eslint ./src ./agents ./lib ./scripts --ext .ts && prettier -c .",
"format": "prettier -w .",
"postbuild": "ts-node scripts/postbuild.ts",
"test": "pnpm lint"
},
"dependencies": {
"handlebars": "^4.7.8",
"js-yaml": "^4.1.0",
"zod": "^3.23.8"
},
"devDependencies": {
"@types/node": "^22.7.4",
"@typescript-eslint/eslint-plugin": "^8.8.1",
"@typescript-eslint/parser": "^8.8.1",
"eslint": "^9.0.0",
"eslint-config-prettier": "^9.1.0",
"prettier": "^3.3.3",
"ts-node": "^10.9.2", "ts-node": "^10.9.2",
"typescript": "^5.6.3" "typescript": "^5.6.3"
} }

13
prompts/brand-kit.yml Normal file
View File

@@ -0,0 +1,13 @@
---
title: Brand Kit Narrative
description: Condense a brand story, tone, and visual system into a shareable doc.
model: gpt-4o
tags: [branding, writing]
steps:
- text: 'Summarize mission, values, and target audience in under 120 words'
- text: 'List a palette of five colors with hex values and usage guidance'
- text: 'Propose a typography stack (display, body, mono) with pairing notes'
- text: "Draft a 6-point voice and tone guide with do/don't bullets"
- text: 'Add a mermaid graph showing brand assets flowing into social, web, print'
notes: |
Keep copy tight but not dry. Include example headlines and a fallback palette for dark mode.

12
prompts/logo-neon.yml Normal file
View File

@@ -0,0 +1,12 @@
---
title: Neon Logo Generator
description: Create a glowing BR-OS logo with golden-ratio curves.
model: gpt-4o
tags: [branding, design]
steps:
- text: 'Generate SVG path for fractal-road glyph'
- text: 'Apply brand gradient #ff4fd8 ➜ #7cf9ff'
- text: 'Return layered SVG wrapped in <svg> with viewBox set to 0 0 1024 1024'
notes: |
Encourage minimal anchor points and smooth Bezier curves. Embed a mermaid diagram if it
helps document the SVG layering order for handoff.

13
prompts/video-intro.yml Normal file
View File

@@ -0,0 +1,13 @@
---
title: Video Intro Script
description: Write a 30-second intro script for a Creator Studio case study reel.
model: gpt-4o
tags: [video, scripting]
steps:
- text: 'Open with a 2-line hook about design velocity and BR-OS automation'
- text: 'List three storyboard beats with timing markers'
- text: 'Propose subtitle text and on-screen motion cues'
- text: 'Return an ffmpeg-friendly filtergraph description for lower third animations'
notes: |
Keep lines under 90 characters for subtitle friendliness. Reference the brand gradient from
`logo-neon.yml` when suggesting color overlays.

4
public/sig.beacon.json Normal file
View File

@@ -0,0 +1,4 @@
{
"ts": "__BUILD_TIMESTAMP__",
"agent": "CreatorPack-Gen-0"
}

12
scripts/postbuild.ts Normal file
View File

@@ -0,0 +1,12 @@
import fs from 'node:fs';
import path from 'node:path';
const beaconPath = path.join(process.cwd(), 'public', 'sig.beacon.json');
const payload = {
ts: new Date().toISOString(),
agent: 'CreatorPack-Gen-0',
};
fs.writeFileSync(beaconPath, JSON.stringify(payload, null, 2));
// TODO(creator-pack-next): Add checksum validation for published artifacts.

132
src/cli.ts Normal file
View File

@@ -0,0 +1,132 @@
import fs from 'node:fs';
import path from 'node:path';
import { loadPromptPreset, dispatchPrompt } from '../agents/generate_prompt.js';
import { renderCanvaBatch, sendToCanva } from '../agents/render_canva.js';
import { renderTemplate } from '../lib/template.js';
const args = process.argv.slice(2);
const command = args[0];
const list = (): void => {
try {
const prompts = fs
.readdirSync(path.join(process.cwd(), 'prompts'))
.filter((file) => file.endsWith('.yml'));
const workflows = fs
.readdirSync(path.join(process.cwd(), 'workflows'))
.filter((file) => file.endsWith('.json.hbs'));
console.log('Prompts:');
prompts.forEach((prompt) => console.log(`- ${prompt.replace('.yml', '')}`));
console.log('\nWorkflows:');
workflows.forEach((flow) => console.log(`- ${flow.replace('.json.hbs', '')}`));
} catch (error) {
console.error('Error listing resources:', (error as Error).message);
console.error('Make sure the prompts/ and workflows/ directories exist.');
process.exit(1);
}
};
const runPrompt = async (slug: string, agentName: string): Promise<void> => {
try {
const preset = loadPromptPreset(slug);
const response = await dispatchPrompt(preset, agentName);
console.log(response);
} catch (error) {
console.error(`Error running prompt '${slug}':`, (error as Error).message);
console.error(`Make sure the file prompts/${slug}.yml exists and is valid.`);
process.exit(1);
}
};
const renderWorkflow = async (templateName: string): Promise<void> => {
try {
const filePath = path.join(process.cwd(), 'workflows', `${templateName}.json.hbs`);
const source = fs.readFileSync(filePath, 'utf-8');
const payload = renderTemplate(source, {
input: 'input.mp4',
output: 'output.mp4',
filters: ['scale=1280:-1', 'format=yuv420p'],
bitrate: '6M',
});
console.log(payload);
} catch (error) {
console.error(`Error rendering workflow '${templateName}':`, (error as Error).message);
console.error(`Make sure the file workflows/${templateName}.json.hbs exists.`);
process.exit(1);
}
};
const renderCanva = async (templateName: string): Promise<void> => {
try {
const payload = renderCanvaBatch(templateName, {
brand: 'Blackroad',
assets: ['cover.png', 'thumbnail.png'],
agent: process.env.CREATOR_AGENT || 'lucidia',
});
const response = await sendToCanva(payload);
console.log(response);
} catch (error) {
console.error(`Error rendering Canva template '${templateName}':`, (error as Error).message);
console.error(`Make sure the file workflows/${templateName}.json.hbs exists.`);
process.exit(1);
}
};
const usage = () => {
console.log('Usage: br-create <command>');
console.log('Commands:');
console.log(' list');
console.log(' run <prompt> [--agent name]');
console.log(' render <workflow>');
console.log(' render-canva <workflow>');
};
const main = async () => {
switch (command) {
case 'list':
list();
break;
case 'run': {
const slug = args[1];
if (!slug) {
console.error('Error: Missing prompt name.');
console.error('Usage: br-create run <prompt> [--agent name]');
process.exit(1);
}
const agentFlagIndex = args.indexOf('--agent');
const agentName =
agentFlagIndex >= 0 && args[agentFlagIndex + 1]
? args[agentFlagIndex + 1]
: process.env.CREATOR_AGENT || 'lucidia';
await runPrompt(slug, agentName);
break;
}
case 'render':
if (!args[1]) {
console.error('Error: Missing workflow name.');
console.error('Usage: br-create render <workflow>');
process.exit(1);
}
await renderWorkflow(args[1]);
break;
case 'render-canva':
if (!args[1]) {
console.error('Error: Missing workflow name.');
console.error('Usage: br-create render-canva <workflow>');
process.exit(1);
}
await renderCanva(args[1]);
break;
default:
usage();
}
};
main().catch((error) => {
console.error('An unexpected error occurred while running the command.');
console.error('Error details:', (error as Error).message);
if ((error as Error).stack) {
console.error('\nStack trace:', (error as Error).stack);
}
process.exit(1);
});

View File

@@ -12,4 +12,17 @@
}, },
"include": ["agents/**/*.ts", "tests/**/*.ts"], "include": ["agents/**/*.ts", "tests/**/*.ts"],
"exclude": ["node_modules"] "exclude": ["node_modules"]
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "Node",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"skipLibCheck": true,
"resolveJsonModule": true,
"outDir": "dist",
"rootDir": "."
},
"include": ["src", "lib", "agents", "scripts"],
"exclude": ["node_modules", "dist"]
} }

View File

@@ -0,0 +1,6 @@
{
"job": "canva-batch",
"agent": "{{agent}}",
"brand": "{{brand}}",
"assets": [{{#each assets}}"{{this}}"{{#unless @last}}, {{/unless}}{{/each}}]
}

View File

@@ -0,0 +1,8 @@
{
"job": "ffmpeg-transcode",
"engine": "ffmpeg",
"input": "{{input}}",
"output": "{{output}}",
"filters": [{{#each filters}}"{{this}}"{{#unless @last}}, {{/unless}}{{/each}}],
"bitrate": "{{bitrate}}"
}