weftens 0.1.2 → 0.1.4

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.
@@ -1,59 +0,0 @@
1
- // content-org — Producer: turns an inbox packet + clips (+ optional voice) into a Reel.
2
- // Stages clips -> runs studio/assemble.sh (FFmpeg) -> queue/<slug>/{<slug>.mp4, manifest.json}.
3
- //
4
- // Run: node producer.mjs <path-to-packet.json>
5
- //
6
- // Inputs it expects (each is its own upstream step, kept out of this file):
7
- // clips/<slug>/*.mp4 — Kayla generates (flat sub) OR a clip-gen API writes them here.
8
- // studio/voice/vo.mp3 — optional narration; generate with your voice step first
9
- // (ElevenLabs or Kokoro) — see SERVICE_SOP.md. Absent -> silent cut.
10
- //
11
- // CONCURRENCY: run at most 2 producers at once (FFmpeg is the only CPU-heavy step);
12
- // set FFMPEG_THREADS=3 to cap each. See CONTENT_ENGINE_TECHNICAL_PLAN.md section 5.
13
-
14
- import { readFileSync, writeFileSync, mkdirSync, existsSync, readdirSync, copyFileSync, renameSync, rmSync } from "node:fs";
15
- import { join } from "node:path";
16
- import { execFileSync } from "node:child_process";
17
-
18
- const packetPath = process.argv[2];
19
- if (!packetPath || !existsSync(packetPath)) {
20
- console.error("Usage: node producer.mjs <path-to-packet.json>");
21
- process.exit(1);
22
- }
23
- const packet = JSON.parse(readFileSync(packetPath, "utf8"));
24
- const slug = packet.slug;
25
- const clipsDir = join("clips", slug);
26
- const outDir = join("queue", slug);
27
- mkdirSync(outDir, { recursive: true });
28
-
29
- // Clips: expect clips/<slug>/*.mp4 (manual, or written by a clip-gen step).
30
- // CLIP-GEN INTEGRATION POINT: drive a fal.ai call (Veo hero / Wan bulk) from the Video
31
- // Editor's shot list, writing numbered mp4s into clipsDir. Kept manual for now.
32
- const haveClips = existsSync(clipsDir) && readdirSync(clipsDir).filter((f) => f.endsWith(".mp4")).length > 0;
33
- if (!haveClips) {
34
- console.error("[clips] none in " + clipsDir + "/ — drop or generate clips there, then re-run.");
35
- writeFileSync(join(outDir, "manifest.json"), JSON.stringify({ ...packet, status: "awaiting-clips" }, null, 2));
36
- process.exit(2);
37
- }
38
- const voiceNote = existsSync("studio/voice/vo.mp3") ? "vo.mp3 present" : "no vo.mp3 (silent cut)";
39
- console.log("[voice] " + voiceNote);
40
-
41
- // Staging must hold ONLY this piece's clips — leftovers from a prior slug end up in the cut.
42
- rmSync("studio/clips", { recursive: true, force: true });
43
- mkdirSync("studio/clips", { recursive: true });
44
- readdirSync(clipsDir).filter((f) => f.endsWith(".mp4")).sort().forEach((f) => copyFileSync(join(clipsDir, f), join("studio/clips", f)));
45
-
46
- try {
47
- execFileSync("bash", ["studio/assemble.sh", slug], { stdio: "inherit", env: { ...process.env } });
48
- const rendered = join("studio/output", slug + ".mp4");
49
- if (existsSync(rendered)) renameSync(rendered, join(outDir, slug + ".mp4"));
50
- } catch (e) {
51
- console.error("[assemble] failed: " + e.message);
52
- }
53
-
54
- const ready = existsSync(join(outDir, slug + ".mp4"));
55
- writeFileSync(
56
- join(outDir, "manifest.json"),
57
- JSON.stringify({ ...packet, reel: ready ? slug + ".mp4" : null, voiceNote, status: ready ? "ready-for-approval" : "assemble-failed" }, null, 2)
58
- );
59
- console.log("[producer] " + slug + ": " + (ready ? "ready-for-approval" : "assemble-failed") + " (approval=" + packet.approval + ")");
@@ -1,34 +0,0 @@
1
- // content-org — Publish approved Reels. `node publish.mjs <slug> [<slug>...]`
2
- // Moves a reviewed Reel from queue/ to a published/ append-only log. The actual post to
3
- // Facebook/Instagram is a marked integration point (needs Meta Business Suite creds).
4
- import { existsSync, readdirSync, readFileSync, appendFileSync, mkdirSync } from "node:fs";
5
- import { join } from "node:path";
6
-
7
- const slugs = process.argv.slice(2);
8
- if (!slugs.length) { console.error("Usage: node publish.mjs <slug> [<slug>...] (or 'auto' to publish all auto-approved)"); process.exit(1); }
9
- mkdirSync("published", { recursive: true });
10
-
11
- let list = slugs;
12
- if (slugs[0] === "auto") {
13
- list = readdirSync("queue").filter((s) => {
14
- const mf = join("queue", s, "manifest.json");
15
- if (!existsSync(mf)) return false;
16
- const m = JSON.parse(readFileSync(mf, "utf8"));
17
- return m.approval === "auto" && (m.qc?.blockers || []).filter(Boolean).length === 0 && m.reel;
18
- });
19
- }
20
-
21
- const stampNote = "(timestamp added by publish step at runtime)";
22
- for (const slug of list) {
23
- const mf = join("queue", slug, "manifest.json");
24
- if (!existsSync(mf)) { console.error(`skip ${slug}: no manifest`); continue; }
25
- const m = JSON.parse(readFileSync(mf, "utf8"));
26
- if (m.approval === "hold") { console.error(`skip ${slug}: HOLD — needs explicit approval, run with the slug to override`); }
27
-
28
- // POSTING INTEGRATION POINT: call Meta Business Suite / Graph API here to publish
29
- // queue/<slug>/<reel>.mp4 with m.feed.caption to the client's page. Left unwired.
30
- const entry = { slug, client: m.client, tier: m.tier, format: m.format, reel: m.reel, caption: m.feed?.caption, note: "logged; Meta post not wired " + stampNote };
31
- appendFileSync(join("published", "log.jsonl"), JSON.stringify(entry) + "\n");
32
- console.log(`published (logged): ${slug} [${m.client || "-"}/${m.tier || "-"}]`);
33
- }
34
- console.log(`\n${list.length} logged to published/log.jsonl. Wire the Meta post at the marked integration point to go live.`);
@@ -1,32 +0,0 @@
1
- // content-org — Review queue. `node review.mjs`
2
- // Lists finished Reels awaiting Kayla, showing tiered-approval status + any QC blockers.
3
- import { existsSync, readdirSync, readFileSync } from "node:fs";
4
- import { join } from "node:path";
5
-
6
- const Q = "queue";
7
- if (!existsSync(Q)) { console.log("queue/ empty — run orchestrator.mjs then producer.mjs first."); process.exit(0); }
8
-
9
- const rows = [];
10
- for (const slug of readdirSync(Q)) {
11
- const mf = join(Q, slug, "manifest.json");
12
- if (!existsSync(mf)) continue;
13
- const m = JSON.parse(readFileSync(mf, "utf8"));
14
- rows.push({
15
- slug, tier: m.tier || "-", client: m.client || "-",
16
- status: m.status || "?", approval: m.approval || "?",
17
- blockers: (m.qc?.blockers || []).filter(Boolean).length,
18
- reel: m.reel ? "yes" : "no",
19
- });
20
- }
21
- if (!rows.length) { console.log("No manifests in queue/."); process.exit(0); }
22
-
23
- console.log("slug".padEnd(22), "client".padEnd(10), "tier".padEnd(8), "approval".padEnd(9), "reel", "blockers", "status");
24
- for (const r of rows) {
25
- console.log(
26
- r.slug.padEnd(22), String(r.client).padEnd(10), String(r.tier).padEnd(8),
27
- r.approval.padEnd(9), (r.reel + " ").padEnd(4), String(r.blockers).padStart(3).padEnd(9), r.status
28
- );
29
- }
30
- const holds = rows.filter((r) => r.approval === "hold" || r.blockers > 0);
31
- console.log(`\n${rows.length} in queue · ${holds.length} need your yes (hold or blocked) · rest auto-postable.`);
32
- console.log("Approve → node publish.mjs <slug> [<slug>...]");
@@ -1,27 +0,0 @@
1
- # The Traveler — Studio (FFmpeg assembly pipeline)
2
-
3
- **Locked stack:** Kling (clips, you) → ElevenLabs (voice, Claude) → **FFmpeg (edit, Claude)**.
4
- Anti-slop = consistent character + Claude-controlled edit, never a preset.
5
-
6
- ## How it works
7
- 1. **You** generate clips in Kling and drop them in `clips/` — **named in order**: `01.mp4`, `02.mp4`, …
8
- 2. Drop the voiceover in `voice/vo.mp3` (Claude can generate this via ElevenLabs).
9
- 3. Drop a music bed in `music/bed.mp3` (royalty-free, e.g. Epidemic).
10
- 4. Captions go in `captions/captions.ass` (Claude writes these from the script).
11
- 5. Claude runs `assemble.sh` → finished vertical Reel lands in `output/`.
12
-
13
- ## CLIP DELIVERY RULES (critical — so it doesn't look like slop)
14
- - **Silent** (no Kling auto-audio) — Claude owns the audio layer.
15
- - **No baked-in captions / no auto-music / no watermark.**
16
- - Vertical, highest resolution Kling allows; consistent across clips.
17
- - **Same locked character reference image every clip** (the recurring face = the moat).
18
- - Name strictly in play order: `01.mp4`, `02.mp4`, …
19
-
20
- ## Folders
21
- - `clips/` — raw Kling footage (you)
22
- - `voice/` — `vo.mp3` narrator (Claude/ElevenLabs)
23
- - `music/` — `bed.mp3` (you, or Mubert)
24
- - `captions/` — `captions.ass` (Claude)
25
- - `output/` — finished Reels (Claude)
26
-
27
- `assemble.sh` is v1 — tuned further once we see real Kling footage.
@@ -1,48 +0,0 @@
1
- #!/usr/bin/env bash
2
- # The Traveler — FFmpeg assembly pipeline (v1)
3
- # Usage: ./assemble.sh [episode_name]
4
- # Reads clips/*.mp4 (in name order) + voice/vo.mp3 + music/bed.mp3 + captions/captions.ass
5
- # Outputs a vertical 1080x1920 Reel to output/<episode_name>.mp4
6
- # Anti-slop layer: uniform grade + film grain + subtle vignette so heterogeneous clips read as ONE piece.
7
- set -euo pipefail
8
-
9
- EP="${1:-episode}"
10
- ROOT="$(cd "$(dirname "$0")" && pwd)"
11
- CLIPS="$ROOT/clips"; VOICE="$ROOT/voice/vo.mp3"; MUSIC="$ROOT/music/bed.mp3"
12
- CAPS="$ROOT/captions/captions.ass"; OUT="$ROOT/output/$EP.mp4"; TMP="$ROOT/.tmp"
13
- W=1080; H=1920; FPS=30
14
- mkdir -p "$TMP" "$ROOT/output"
15
-
16
- # 1) Normalize every clip to uniform res/fps/codec (reliable concat across mixed Kling outputs)
17
- i=0; : > "$TMP/list.txt"
18
- for f in $(ls "$CLIPS"/*.mp4 2>/dev/null | sort); do
19
- i=$((i+1)); n=$(printf "%03d" "$i")
20
- ffmpeg -y -i "$f" -vf "scale=$W:$H:force_original_aspect_ratio=increase,crop=$W:$H,fps=$FPS" \
21
- -c:v libx264 -preset medium -crf 18 -an "$TMP/n_$n.mp4"
22
- # Windows fix: write the entry relative to list.txt (concat demuxer resolves it from
23
- # the list's own directory). Absolute Git-Bash paths (/c/...) break native ffmpeg.exe.
24
- echo "file 'n_$n.mp4'" >> "$TMP/list.txt"
25
- done
26
- [ "$i" -gt 0 ] || { echo "No clips in $CLIPS (name them 01.mp4, 02.mp4 ...)"; exit 1; }
27
-
28
- # 2) Concatenate normalized clips (video only)
29
- ffmpeg -y -f concat -safe 0 -i "$TMP/list.txt" -c copy "$TMP/video.mp4"
30
-
31
- # 3) Grade + grain + vignette (cohesion / anti-slop) and burn captions if present
32
- GRADE="eq=contrast=1.06:saturation=1.05:gamma=0.98,noise=alls=8:allf=t+u,vignette=PI/5"
33
- if [ -f "$CAPS" ]; then VF="$GRADE,subtitles='$CAPS'"; else VF="$GRADE"; fi
34
- ffmpeg -y -i "$TMP/video.mp4" -vf "$VF" -c:v libx264 -preset medium -crf 18 -an "$TMP/graded.mp4"
35
-
36
- # 4) Mix audio: narrator full + music bed ducked under it
37
- if [ -f "$VOICE" ] && [ -f "$MUSIC" ]; then
38
- ffmpeg -y -i "$TMP/graded.mp4" -i "$VOICE" -i "$MUSIC" -filter_complex \
39
- "[2:a]volume=0.18[m];[1:a][m]amix=inputs=2:duration=first:dropout_transition=2[a]" \
40
- -map 0:v -map "[a]" -c:v copy -c:a aac -shortest "$OUT"
41
- elif [ -f "$VOICE" ]; then
42
- ffmpeg -y -i "$TMP/graded.mp4" -i "$VOICE" -map 0:v -map 1:a -c:v copy -c:a aac -shortest "$OUT"
43
- else
44
- cp "$TMP/graded.mp4" "$OUT"; echo "(no voice/vo.mp3 — output is silent)"
45
- fi
46
-
47
- rm -rf "$TMP"
48
- echo "Done -> $OUT"
@@ -1,110 +0,0 @@
1
- // content-org — no-dependency test script (plain node, no vitest).
2
- // Run: npm test
3
- // Covers: (1) seat schema validation for all 10 seats in dry-run,
4
- // (2) single-seat CLI mode, (3) review/publish queue round-trip.
5
- // The queue round-trip runs in a scratch dir so it never touches the real queue/.
6
-
7
- import { execFileSync } from "node:child_process";
8
- import { mkdirSync, writeFileSync, readFileSync, rmSync, mkdtempSync } from "node:fs";
9
- import { join, dirname } from "node:path";
10
- import { fileURLToPath } from "node:url";
11
- import { tmpdir } from "node:os";
12
- import { SEATS, stub } from "./orchestrator.mjs";
13
- import { SEAT_MODEL, MODELS } from "./config.mjs";
14
-
15
- const ROOT = dirname(fileURLToPath(import.meta.url));
16
- let pass = 0, fail = 0;
17
- function check(name, ok, detail = "") {
18
- if (ok) { pass++; console.log(` ok ${name}`); }
19
- else { fail++; console.error(`FAIL ${name}${detail ? " — " + detail : ""}`); }
20
- }
21
-
22
- // Minimal validator for the subset of JSON Schema the seats use.
23
- function validate(value, schema, path = "$") {
24
- const errs = [];
25
- if (schema.type === "string") { if (typeof value !== "string") errs.push(`${path}: not a string`); return errs; }
26
- if (schema.type === "boolean") { if (typeof value !== "boolean") errs.push(`${path}: not a boolean`); return errs; }
27
- if (schema.type === "number") { if (typeof value !== "number") errs.push(`${path}: not a number`); return errs; }
28
- if (schema.type === "array") {
29
- if (!Array.isArray(value)) { errs.push(`${path}: not an array`); return errs; }
30
- value.forEach((v, i) => errs.push(...validate(v, schema.items, `${path}[${i}]`)));
31
- return errs;
32
- }
33
- if (schema.type === "object") {
34
- if (typeof value !== "object" || value === null || Array.isArray(value)) { errs.push(`${path}: not an object`); return errs; }
35
- for (const req of schema.required || []) if (!(req in value)) errs.push(`${path}.${req}: missing (required)`);
36
- for (const [k, v] of Object.entries(value)) {
37
- if (!schema.properties[k]) { if (schema.additionalProperties === false) errs.push(`${path}.${k}: unexpected property`); continue; }
38
- errs.push(...validate(v, schema.properties[k], `${path}.${k}`));
39
- }
40
- return errs;
41
- }
42
- errs.push(`${path}: unknown schema type ${schema.type}`);
43
- return errs;
44
- }
45
-
46
- // --- 1) All 9 seats: dry-run stub output conforms to each seat's own schema ------------
47
- console.log("\n[1] seat schemas (dry-run stubs) — 10 seats");
48
- const EXPECTED_SEATS = ["showrunner", "strategy", "ideator", "research", "script", "editor", "cut", "feed", "qc", "growth"];
49
- check("all 10 seats registered", EXPECTED_SEATS.every((k) => SEATS[k]) && Object.keys(SEATS).length === 10,
50
- `have: ${Object.keys(SEATS).join(",")}`);
51
- for (const key of EXPECTED_SEATS) {
52
- const seat = SEATS[key];
53
- if (!seat) { check(`seat ${key}`, false, "missing"); continue; }
54
- const out = stub(seat.schema, key);
55
- const errs = validate(out, seat.schema);
56
- check(`stub(${key}) matches schema`, errs.length === 0, errs.join("; "));
57
- check(`${key} routes to a real model`, !!MODELS[SEAT_MODEL[key]], `SEAT_MODEL.${key}=${SEAT_MODEL[key]}`);
58
- }
59
-
60
- // --- 2) Single-seat CLI mode (dry-run, no API key needed) ------------------------------
61
- console.log("\n[2] single-seat CLI mode");
62
- for (const key of ["showrunner", "qc"]) {
63
- let out, parsed = null, errs = ["did not run"];
64
- try {
65
- out = execFileSync(process.execPath, ["orchestrator.mjs", "--seat", key, "--input", '{"format":"scam-reveal"}', "--dry-run"],
66
- { cwd: ROOT, encoding: "utf8" });
67
- parsed = JSON.parse(out);
68
- errs = validate(parsed, SEATS[key].schema);
69
- } catch (e) { errs = [e.message]; }
70
- check(`--seat ${key} --dry-run emits schema-valid JSON`, parsed !== null && errs.length === 0, errs.join("; "));
71
- }
72
- // unknown seat must exit non-zero
73
- let badExit = false;
74
- try { execFileSync(process.execPath, ["orchestrator.mjs", "--seat", "nope", "--input", "x", "--dry-run"], { cwd: ROOT, stdio: "pipe" }); }
75
- catch { badExit = true; }
76
- check("--seat with unknown key exits non-zero", badExit);
77
-
78
- // --- 3) review/publish queue round-trip (scratch dir; real queue/ untouched) -----------
79
- console.log("\n[3] review/publish queue round-trip");
80
- const scratch = mkdtempSync(join(tmpdir(), "content-org-test-"));
81
- try {
82
- const slug = "test-piece";
83
- mkdirSync(join(scratch, "queue", slug), { recursive: true });
84
- const manifest = {
85
- slug, format: "nostalgia", client: "testco", tier: "starter",
86
- approval: "auto", reel: `${slug}.mp4`, status: "ready-for-approval",
87
- qc: { blockers: [] }, feed: { caption: "test caption" },
88
- };
89
- writeFileSync(join(scratch, "queue", slug, "manifest.json"), JSON.stringify(manifest, null, 2));
90
-
91
- const reviewOut = execFileSync(process.execPath, [join(ROOT, "review.mjs")], { cwd: scratch, encoding: "utf8" });
92
- check("review.mjs lists the queued piece", reviewOut.includes(slug) && reviewOut.includes("1 in queue"));
93
- check("review.mjs shows auto approval", /auto/.test(reviewOut));
94
-
95
- execFileSync(process.execPath, [join(ROOT, "publish.mjs"), slug], { cwd: scratch, encoding: "utf8" });
96
- const log = readFileSync(join(scratch, "published", "log.jsonl"), "utf8").trim().split("\n").map((l) => JSON.parse(l));
97
- check("publish.mjs appends to published/log.jsonl", log.length === 1 && log[0].slug === slug);
98
- check("published entry carries caption + reel", log[0].caption === "test caption" && log[0].reel === `${slug}.mp4`);
99
- check("published entry marks Meta post as not wired", /not wired/.test(log[0].note || ""));
100
-
101
- // 'auto' mode publishes only clean auto-approved pieces
102
- execFileSync(process.execPath, [join(ROOT, "publish.mjs"), "auto"], { cwd: scratch, encoding: "utf8" });
103
- const log2 = readFileSync(join(scratch, "published", "log.jsonl"), "utf8").trim().split("\n");
104
- check("publish.mjs auto mode picks up the auto-approved piece", log2.length === 2);
105
- } finally {
106
- rmSync(scratch, { recursive: true, force: true });
107
- }
108
-
109
- console.log(`\n${pass} passed, ${fail} failed`);
110
- process.exit(fail ? 1 : 0);