Skip to content

Repository files navigation

contentforge

Turn one long-form asset into a full week of channel-native content — in a defined brand voice, with the source timecode attached to every piece.

🔗 Live demo — paste your own transcript, no signup, no API key (the demo runs the entire engine in your browser; nothing you paste leaves your machine)


The problem this solves

Founders and marketing teams record podcasts, webinars, customer interviews and conference talks. Then they publish each one once, and the other twenty ideas inside that recording die in a Google Drive folder.

Repurposing is the highest-ROI content work there is, and almost nobody does it, for one boring reason: it is four hours of scrubbing through a recording trying to remember where the good part was. The thinking already happened in the room. What is missing is the machinery that turns 90 minutes of raw material into channel-native assets a human can approve in ten minutes.

contentforge is that machinery:

  • In: a transcript, a caption file (.srt / .vtt), or a long blog post.
  • Out: 3 LinkedIn posts (story / listicle / contrarian), an 8-12 post X thread, a newsletter section with subject-line options, 5 short-form video scripts with hooks, beats, on-screen text, B-roll notes and the exact source timecode to cut, a YouTube title/description/chapter package, 5 quote cards, and a 7-day publishing calendar — plus markdown / JSON / CSV exports.
  • Every asset is checked against real platform limits and scored against a brand voice profile, with every violation surfaced and explained.

Who buys this

Buyer The job it does
Content agencies Turn a per-recording retainer into a per-channel retainer. One recording becomes a deliverable pack a client can see the value of.
Podcasters and video creators Stop publishing once. Clip suggestions come with timecodes, so the editor's job goes from "watch 90 minutes" to "cut these five ranges".
Founder-led B2B brands The founder's actual opinions, in the founder's actual voice, on five channels a week, without the founder writing five posts a week.
Video editors upselling retainers Add "and we'll write the posts" to an editing retainer with no extra headcount.
In-house marketing teams of one The single-person content team's capacity problem is triage, not writing. This does the triage.

Outcome metrics to quote in a proposal

  • 1 recording → 20+ publishable assets in minutes (16 assets + a 7-day schedule from a normal-length recording; the count is configurable, and a very short source yields fewer quote cards rather than padded ones).
  • Editor time per clip: ~40 min → ~5 min. The clip list arrives as real timecode ranges with a scored justification.
  • Zero platform-limit failures. Every asset is validated against LinkedIn's 3,000 characters, X's 280 per post, YouTube's 100-character titles, the 60-character on-screen caption budget, and so on.
  • Voice compliance is a number, not an opinion. Every asset carries a 0-100 score against the client's profile, with the exact phrase that broke the rule.

Architecture

flowchart TD
    A["Source<br/>.txt · .md · .srt · .vtt · pasted text"] --> B[Parser]
    B -->|"cues + timecodes"| C["Segmenter<br/>vocabulary-drift boundaries"]
    C --> D["Insight extraction<br/>claims · stats · stories · contrarian · quotes"]
    C --> E["Clip scorer<br/>emotion · specificity · story · standalone · length"]
    D --> F[Brief]
    E --> F
    G["Brand voice profile<br/>tone · banned words · reading level · CTA · POV"] --> F
    F --> H{"LLMProvider"}
    H -->|"DEMO_MODE=true (default)"| I["MockProvider<br/>deterministic extractive composition"]
    H -->|"DEMO_MODE=false + API key"| J["AnthropicProvider<br/>claude-sonnet-5, tool-use structured output"]
    I --> K[Generators]
    J --> K
    K --> L["LinkedIn ×3"]
    K --> M["X thread"]
    K --> N["Newsletter"]
    K --> O["Short-form ×5 (timecoded)"]
    K --> P["YouTube package"]
    K --> Q["Quote cards ×5"]
    L & M & N & O & P & Q --> R["Voice checker<br/>0-100 + violations"]
    R --> S["Platform constraint checks"]
    S --> T["7-day calendar<br/>per-channel cadence + optimal times"]
    T --> U["Exports<br/>markdown · JSON · scheduler CSV · one file per asset"]
    T --> V["SQLite pack store"]
Loading

Both provider paths share the same JSON Schema and the same validator, so the demo output and the production output are structurally identical — only the prose differs.


2-minute quickstart

git clone https://github.com/wasay-09/contentforge.git
cd contentforge
npm install

# 1. Look at what the analyser sees (no key needed, nothing is sent anywhere)
npm run cli -- analyse fixtures/founder-podcast.srt

# 2. Generate a full pack to ./out
npm run cli -- forge fixtures/founder-podcast.srt --voice blunt-technical-founder --out ./out
open out/pack.md          # markdown pack, JSON, schedule.csv, calendar.csv, one file per asset

# 3. Or run the web app
npm run build && npm start   # http://localhost:3000

Everything above works with no API key and no network access. That is the default, not a fallback.

# Optional: use Claude for the copywriting instead of the deterministic engine
cp .env.example .env
# set DEMO_MODE=false and ANTHROPIC_API_KEY=sk-ant-...
npm start

Docker:

docker compose up --build      # http://localhost:3000

Zero-key demo mode

DEMO_MODE=true is the default. In this mode a MockProvider composes every asset extractively from the real analysis — it selects the strongest quotes, statistics, anecdotes and contrarian takes out of the actual source, arranges them into channel-native structures, applies the brand voice's connectives and CTA style, strips banned phrasing, and enforces platform limits.

It is not lorem ipsum and it is not a stub. It is the same code path, the same schema and the same validator the Claude path uses — which is exactly why it is the default: you can evaluate the whole product, on your own transcript, before spending a cent.

With DEMO_MODE=false and ANTHROPIC_API_KEY set, AnthropicProvider asks claude-sonnet-5 for the same structures via forced tool use, validates the response against the same schema, repairs malformed output by feeding the validation error back to the model, retries transient API failures with backoff, and falls back to the deterministic composition if the model cannot produce valid output — so a client pack is never empty.


Brand voice configuration

Three profiles ship in src/voice/profiles.ts: blunt-technical-founder, warm-b2c-brand, formal-enterprise. A profile is plain data:

Field Type What it does
id / name / description string Identity. description is fed to the model in the system prompt.
tone string[] Tone adjectives, surfaced in the system prompt.
bannedWords string[] Phrases that must never appear. Matched case-insensitively; each hit is an error in the compliance report and is stripped from borrowed source text by the deterministic composer.
preferredVocabulary string[] Words the brand reaches for. Presence raises the compliance score (max +6).
readingLevel.targetGrade / .tolerance number Flesch-Kincaid target. Drift beyond tolerance is a warning; beyond 2× tolerance it is an error.
emoji.policy / .maxPerAsset "none" | "sparing" | "liberal", number Hard cap on emoji per asset. none also strips emoji from borrowed text.
cta.style / .examples "direct" | "soft" | "question" | "none", string[] CTA voice. The deterministic composer rotates the examples; the model is shown them as the target feel.
pov first-person-singular | first-person-plural | second-person | third-person Drives connective phrasing ("Here's what actually happened" vs "What we observed") and a POV compliance check.
sentenceMaxWords number Sentences above this are flagged.
formatting.allowHashtags / maxHashtags / allowExclamations / allowAllCaps boolean/number Formatting policy, enforced by the checker.

The compliance checker returns { score, grade, violations[], metrics }. Score starts at 100, loses 12 per error and 5 per warning, gains up to 6 for preferred vocabulary. grade is on-voice (≥85), close (≥70) or off-voice.

Adding a client voice is a 5-minute job — see DEPLOY.md § (d).


Clip-worthiness scoring

Short-form suggestions are only useful if you can see why something was suggested. The score is a transparent weighted sum of five normalised signals (src/analysis/clipScore.ts):

Signal Weight Measures
specificity 0.26 Numbers, named entities and concrete business nouns per word.
emotion 0.22 Density of high-arousal language.
standalone 0.22 Whether it survives being dropped into a feed with zero context (penalises anaphora and pronoun density).
story 0.18 Density of narrative markers ("one day", "turns out", "we tried").
length 0.12 Proximity to a comfortable 30-75 second read (full marks 60-180 words).

Every segment returns its components and a plain-English explanation of what drove the number. The word lists live in src/analysis/lexicon.ts and are meant to be edited per client domain.


Channel and constraint matrix

Channel Assets Constraints enforced Timecoded
LinkedIn 3 posts: story, listicle, contrarian 3,000 char post limit; hook must stand alone in the 210-char pre-fold window ✅ source range
X / Twitter 1 thread, 8-12 posts 280 chars per post including the n/m counter ✅ source range
Newsletter 1 section + 5 subject lines + preview 60-char subjects, 100-char preview, ~350-word body ✅ source range
Short-form video 5 scripts: 3-second hook, beats, on-screen text, B-roll, CTA, target duration 60-second runtime, 60-char on-screen captions exact cut range
YouTube 5 title options + description + chapters 100-char titles, 5,000-char description, first chapter pinned to 00:00 ✅ chapters from real cues
Quote cards 5 cards + visual treatment 220 chars ✅ interpolated to the sentence

Sources without timecodes (blog posts) still get chapters and clip suggestions — derived from spoken-word pacing and clearly labelled as estimated rather than presented as exact.


Exports

Format Command / endpoint Shape
Markdown pack out/pack.md, GET /api/packs/:id/export/md Client-ready document: analysis table, every asset, the calendar.
JSON out/pack.json, .../export/json The full ContentPack object — analysis, assets, voice reports, constraints, calendar.
Scheduler CSV out/schedule.csv, .../export/csv Buffer / Hypefury-shaped: channel, scheduled_at_utc, scheduled_local, timezone, text, title, asset_id, format, voice_score, source_timecode.
Calendar CSV out/calendar.csv, .../export/calendar.csv Day-by-day plan with the rationale for each slot.
One file per asset out/assets/<channel>/NN-<format>-<slug>.txt For handing individual pieces to individual people.

HTTP API

Method Path Purpose
GET / Web UI
GET /health Status, active provider, available voices
GET /api/voices Voice profiles
GET /api/samples, /api/samples/:id Bundled sample sources
POST /api/analyse {source | sampleId, filename?} → parse + segment analysis only
POST /api/forge {source | sampleId, voice?, timezone?, startDate?, filename?} → full pack (persisted)
POST /api/voice-check {text, voice?} → compliance report for arbitrary copy
GET /api/packs, /api/packs/:id, DELETE /api/packs/:id Pack store
GET /api/packs/:id/export/:format md · json · csv · calendar.csv

How this is deployed for a client

  1. Voice capture (half a day). Take 10-20 pieces of the client's best-performing existing copy. Write the profile: banned phrases come from what makes them wince, preferred vocabulary from what they actually say, reading level from measuring their own posts. Commit it as src/voice/profiles.ts entry or a JSON file in their repo.
  2. Calibrate the lexicon (1 hour). src/analysis/lexicon.ts ships a general business vocabulary. Add the client's domain nouns to CONCRETE_MARKERS so specificity scoring recognises their world (SKU, cohort, admission, claim, dispatch…).
  3. Deploy. docker compose up -d on their box, or Railway/Render/Fly from DEPLOY.md. Demo mode is fine for the pilot; switch on ANTHROPIC_API_KEY when they want the copy polished by a model.
  4. Wire the intake. Their editor drops the transcript (most transcription tools export .srt / .vtt) into the UI or posts it to /api/forge. Pack lands in SQLite with a permalink.
  5. Review loop. The client reviews the markdown pack. Anything scoring below 85 on voice gets read first — that is where the checker is telling you the profile and the copy disagree. Feed the corrections back into the profile; the score is the retro.
  6. Publishing. Export the scheduler CSV into Buffer/Hypefury, or hand the per-asset files to whoever owns each channel. The short-form scripts go to the editor with the timecodes already attached.

Retainer framing that works: the pilot is one recording, priced as a one-off. The retainer is per-recording thereafter, because the value scales with volume and the setup cost is paid once.


Development

npm run typecheck     # tsc --noEmit, strict
npm test              # vitest
npm run verify:demo   # rebuild docs/index.html from fixtures and run its engine headlessly
npm run check         # all three

Layout:

src/parse/        transcript + caption parsing (SRT/VTT/markdown/text)
src/analysis/     segmentation, insight extraction, clip scoring, lexicon
src/voice/        brand voice profiles + compliance checker
src/generators/   one module per channel + shared brief and platform constraints
src/llm/          LLMProvider interface, MockProvider, AnthropicProvider
src/schedule/     7-day calendar with per-channel cadence and timezone maths
src/export/       markdown, JSON, CSV, per-asset files
src/db/           SQLite pack store
src/web/          web UI served by the app
scripts/          static demo build + headless verification
docs/index.html   the live demo (generated; commit the output)

Known limitations

  • The deterministic engine is extractive. It selects, arranges and frames the source's own sentences — it does not invent new phrasing. That is a deliberate trade (it cannot hallucinate a statistic), but the prose has the cadence of the speaker, not of a copywriter. Turn on the Anthropic path when you want it rewritten rather than rearranged.
  • The analysers are tuned for English business/marketing speech. Other languages parse fine but the lexicon-driven signals (emotion, story, contrarian) will under-fire until the word lists are localised.
  • Clip suggestions are segment-level, not frame-level. They tell an editor which 30-90 second range to open, not the exact in/out points.
  • Optimal publishing times are sensible defaults, not your audience's analytics. Treat them as a starting cadence and replace them with the client's own data when you have it.
  • The Anthropic path is written carefully against the current SDK but was developed without a key available, so the mock path is the one with test coverage.
  • The Dockerfile and docker-compose.yml are written against a standard Node 22 + better-sqlite3 build, but were not executed on the machine this was built on (no Docker daemon available). The compiled dist/ output they run is verified.

Licence

MIT.

About

Content repurposing engine — turn one long-form asset into a full multi-channel content pack in a defined brand voice

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages