vibemovie 0.1.0 → 0.2.0
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.
- package/dist/{chunk-JTRTKTPK.js → chunk-DO2IWHL6.js} +370 -5
- package/dist/cinematic-C31_2Fw6.d.ts +219 -0
- package/dist/cli.d.ts +7 -4
- package/dist/cli.js +31 -9
- package/dist/index.d.ts +44 -18
- package/dist/index.js +11 -1
- package/dist/mcp.d.ts +5 -3
- package/dist/mcp.js +13 -5
- package/package.json +3 -3
- package/dist/scenes-CKgKppfK.d.ts +0 -118
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
|
-
import { createCascade, createConsentLedger } from "@pooriaarab/vibe-core";
|
|
2
|
+
import { createCascade, createConsentLedger, makeEvent, notify as vibeCoreNotify } from "@pooriaarab/vibe-core";
|
|
3
3
|
import { writeFile } from "fs/promises";
|
|
4
4
|
|
|
5
5
|
// src/scenes.ts
|
|
@@ -687,6 +687,327 @@ function renderHyperframes(scenes, opts = {}) {
|
|
|
687
687
|
`;
|
|
688
688
|
}
|
|
689
689
|
|
|
690
|
+
// src/cinematic.ts
|
|
691
|
+
import { execFileSync } from "child_process";
|
|
692
|
+
import { copyFileSync, existsSync, mkdirSync, mkdtempSync, readFileSync, writeFileSync } from "fs";
|
|
693
|
+
import { tmpdir } from "os";
|
|
694
|
+
import { basename as basename2, join } from "path";
|
|
695
|
+
var ENGINES = ["hyperframes", "cinematic"];
|
|
696
|
+
var STYLE = "cinematic 1980s film still, warm tungsten light with subtle magenta and cyan neon accents, 35mm film grain, shallow depth of field, night";
|
|
697
|
+
var SET = "the SAME single cozy 1980s home office at night: a wooden desk front and center with a glowing laptop and mechanical keyboard, a steaming mug, sticky notes on the wall behind it, a warm desk lamp on the left, and a bookshelf with a glowing retro radio on the right";
|
|
698
|
+
var DEV = "a young woman software developer in her early twenties, big voluminous curly blonde 80s hair, natural makeup with soft neutral eyeshadow and red lipstick, wearing a VIBRANT colorful 80s outfit: a bold color-blocked cropped jacket in hot pink and electric blue over a bright teal tee, vibrant and colorful";
|
|
699
|
+
var LOCK = "Identical woman and identical office as the reference (same face, big curly blonde hair, natural red-lip makeup, vibrant hot-pink-and-blue cropped jacket, teal tee, same desk/laptop/lamp/sticky-notes/bookshelf). Only change pose and camera.";
|
|
700
|
+
var EYE = "looking at what she is doing, not at the camera";
|
|
701
|
+
function plural2(n, word) {
|
|
702
|
+
return `${n} ${word}${n === 1 ? "" : "s"}`;
|
|
703
|
+
}
|
|
704
|
+
function deriveReferencePrompts() {
|
|
705
|
+
return {
|
|
706
|
+
hero: `${STYLE}. Wide reference of ${DEV}, settling in to work at ${SET}.`,
|
|
707
|
+
face: `${STYLE}. Clean sharp front-facing headshot portrait of ${DEV}, looking straight at camera, evenly lit, crisp focus on her face.`
|
|
708
|
+
};
|
|
709
|
+
}
|
|
710
|
+
function beatFor(scene2) {
|
|
711
|
+
switch (scene2.kind) {
|
|
712
|
+
case "title": {
|
|
713
|
+
const d = scene2.data;
|
|
714
|
+
const dur = d.totalMinutes > 0 ? `${formatMinutes(d.totalMinutes)} in the flow` : "a session worth replaying";
|
|
715
|
+
return {
|
|
716
|
+
kind: scene2.kind,
|
|
717
|
+
keyframe: `${LOCK} Wide establishing shot from across the room: she drops into her desk chair and flips open the glowing laptop, screen light spilling onto her face, ${EYE}. ${STYLE}`,
|
|
718
|
+
motion: `she sits down at the desk and opens the laptop, the screen wakes and lights her face, smooth settle-in. ${STYLE}`,
|
|
719
|
+
vo: `[energetically] ${d.sessionName} \u2014 ${dur}. Let's run it back.`
|
|
720
|
+
};
|
|
721
|
+
}
|
|
722
|
+
case "tasks": {
|
|
723
|
+
const d = scene2.data;
|
|
724
|
+
const first = d.tasks[0]?.label ?? "the first task";
|
|
725
|
+
return {
|
|
726
|
+
kind: scene2.kind,
|
|
727
|
+
keyframe: `${LOCK} Close-up side profile: she types fast on the mechanical keyboard, then reaches up and ticks a sticky note on the wall, mid-action, ${EYE}. ${STYLE}`,
|
|
728
|
+
motion: `she types quickly, then reaches over and checks off a sticky note, natural working rhythm. ${STYLE}`,
|
|
729
|
+
vo: d.total === 1 ? `[excited] One task landed, clean \u2014 ${first}.` : `[excited] ${plural2(d.total, "task")} landed, clean \u2014 kicked off by ${first}.`
|
|
730
|
+
};
|
|
731
|
+
}
|
|
732
|
+
case "diff": {
|
|
733
|
+
const d = scene2.data;
|
|
734
|
+
return {
|
|
735
|
+
kind: scene2.kind,
|
|
736
|
+
keyframe: `${LOCK} Over-the-shoulder shot from behind her: she leans into the glowing laptop covered in code, one hand mid-scroll, screen glow catching her hair, ${EYE}. ${STYLE}`,
|
|
737
|
+
motion: `she scrolls through the diff and leans in closer to the code, subtle continuous motion. ${STYLE}`,
|
|
738
|
+
vo: `[excited] ${plural2(d.filesChanged, "file")} touched \u2014 plus ${d.additions}, minus ${d.deletions}.`
|
|
739
|
+
};
|
|
740
|
+
}
|
|
741
|
+
case "terminal": {
|
|
742
|
+
const d = scene2.data;
|
|
743
|
+
const hasTests = d.lines.some((l) => l.cls === "ok" && l.text.includes("passed"));
|
|
744
|
+
return {
|
|
745
|
+
kind: scene2.kind,
|
|
746
|
+
keyframe: `${LOCK} Three-quarter shot from the lamp side: she throws a triumphant fist pump at the glowing screen, mid-cheer, ${EYE}. ${STYLE}`,
|
|
747
|
+
motion: `she reads the screen, then pumps her fist in celebration, energetic. ${STYLE}`,
|
|
748
|
+
vo: hasTests ? "[shouting] GREEN! Green across the board \u2014 every test, passing!" : "[excited] The commands tell the story \u2014 clean, top to bottom."
|
|
749
|
+
};
|
|
750
|
+
}
|
|
751
|
+
case "merge": {
|
|
752
|
+
const d = scene2.data;
|
|
753
|
+
const vo = d.pr !== null ? d.merged ? `[excited] And there it is \u2014 pull request number ${d.pr}, merged into ${d.branch}! No conflicts, no mercy!` : `[excited] Pull request number ${d.pr} is open on ${d.branch} \u2014 reviews incoming!` : d.merged ? `[excited] Merged into ${d.branch}! No conflicts, no mercy!` : `[excited] The pull request is open on ${d.branch}!`;
|
|
754
|
+
return {
|
|
755
|
+
kind: scene2.kind,
|
|
756
|
+
keyframe: `${LOCK} Low-angle shot from the desk surface: she hits the enter key with a flourish and throws both hands up, screen glow flaring, mid-celebration, ${EYE}. ${STYLE}`,
|
|
757
|
+
motion: `she slams enter, throws both hands up and leans back laughing, joyful release. ${STYLE}`,
|
|
758
|
+
vo
|
|
759
|
+
};
|
|
760
|
+
}
|
|
761
|
+
case "end": {
|
|
762
|
+
return {
|
|
763
|
+
kind: scene2.kind,
|
|
764
|
+
keyframe: `${LOCK} Wide slightly dutch-angle shot: she closes the laptop and leans back in her chair with a satisfied smile, lamplight warm on the room, ${EYE}. ${STYLE}`,
|
|
765
|
+
motion: `she gently closes the laptop and leans back, smiling, calm settle. ${STYLE}`,
|
|
766
|
+
vo: `[warmly] ${scene2.caption} This is vibemovie.`
|
|
767
|
+
};
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
function deriveBeats(scenes) {
|
|
772
|
+
return scenes.map(beatFor);
|
|
773
|
+
}
|
|
774
|
+
function cinematicAvailable(env = process.env) {
|
|
775
|
+
const key = env["WAVESPEED_API_KEY"];
|
|
776
|
+
return typeof key === "string" && key.trim().length > 0;
|
|
777
|
+
}
|
|
778
|
+
var defaultExec = (file, args) => {
|
|
779
|
+
execFileSync(file, [...args], { stdio: "ignore" });
|
|
780
|
+
};
|
|
781
|
+
function ffmpegAvailable(execFn = defaultExec) {
|
|
782
|
+
try {
|
|
783
|
+
execFn("ffmpeg", ["-version"]);
|
|
784
|
+
return true;
|
|
785
|
+
} catch {
|
|
786
|
+
return false;
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
var API_BASE = "https://api.wavespeed.ai/api/v3";
|
|
790
|
+
var MODEL_T2I = "bytedance/seedream-v4";
|
|
791
|
+
var MODEL_EDIT = "wavespeed-ai/flux-kontext-max";
|
|
792
|
+
var MODEL_I2V = "kwaivgi/kling-v2.5-turbo-pro/image-to-video";
|
|
793
|
+
var MODEL_FACE_SWAP = "wavespeed-ai/video-face-swap";
|
|
794
|
+
var MODEL_TTS = "elevenlabs/eleven-v3";
|
|
795
|
+
var T2I_SIZE = "1920*1080";
|
|
796
|
+
var CLIP_SECONDS = 5;
|
|
797
|
+
var NEGATIVE_PROMPT = "morphing, warping, distorted face, changing outfit, extra fingers, flicker, duplicated objects, ghosting, double image, cloned furniture";
|
|
798
|
+
var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
799
|
+
async function renderCinematic(scenes, opts) {
|
|
800
|
+
const apiKey = opts.apiKey ?? process.env["WAVESPEED_API_KEY"];
|
|
801
|
+
if (typeof apiKey !== "string" || apiKey.trim().length === 0) {
|
|
802
|
+
throw new Error("cinematic: WAVESPEED_API_KEY is required (set it in the environment or pass opts.apiKey)");
|
|
803
|
+
}
|
|
804
|
+
const fetchFn = opts.deps?.fetchFn ?? fetch;
|
|
805
|
+
const execFn = opts.deps?.execFn ?? defaultExec;
|
|
806
|
+
const sleepFn = opts.deps?.sleepFn ?? sleep;
|
|
807
|
+
const log = opts.log ?? ((msg) => process.stderr.write(`${msg}
|
|
808
|
+
`));
|
|
809
|
+
const voiceId = opts.voiceId ?? "George";
|
|
810
|
+
const workDir = opts.workDir ?? mkdtempSync(join(tmpdir(), "vibemovie-cinematic-"));
|
|
811
|
+
mkdirSync(workDir, { recursive: true });
|
|
812
|
+
const headers = { Authorization: `Bearer ${apiKey}` };
|
|
813
|
+
const post = async (model, body) => {
|
|
814
|
+
const res = await fetchFn(`${API_BASE}/${model}`, {
|
|
815
|
+
method: "POST",
|
|
816
|
+
headers: { ...headers, "Content-Type": "application/json" },
|
|
817
|
+
body: JSON.stringify(body)
|
|
818
|
+
});
|
|
819
|
+
const j = await res.json();
|
|
820
|
+
const id = j.data?.id;
|
|
821
|
+
if (id === void 0) throw new Error(`cinematic: ${model} did not return a prediction id`);
|
|
822
|
+
return id;
|
|
823
|
+
};
|
|
824
|
+
const poll = async (id, maxTicks = 95, intervalMs = 3e3) => {
|
|
825
|
+
for (let i = 0; i < maxTicks; i++) {
|
|
826
|
+
const res = await fetchFn(`${API_BASE}/predictions/${id}/result`, { headers });
|
|
827
|
+
const j = await res.json();
|
|
828
|
+
const status = j.data?.status;
|
|
829
|
+
const out = j.data?.outputs?.[0];
|
|
830
|
+
if (status === "completed" && out !== void 0) return out;
|
|
831
|
+
if (status === "failed") throw new Error(`cinematic: prediction ${id} failed`);
|
|
832
|
+
await sleepFn(intervalMs);
|
|
833
|
+
}
|
|
834
|
+
throw new Error(`cinematic: prediction ${id} timed out`);
|
|
835
|
+
};
|
|
836
|
+
const download = async (url, path) => {
|
|
837
|
+
const res = await fetchFn(url);
|
|
838
|
+
writeFileSync(path, Buffer.from(await res.arrayBuffer()));
|
|
839
|
+
};
|
|
840
|
+
const upload = async (path) => {
|
|
841
|
+
const form = new FormData();
|
|
842
|
+
form.append("file", new Blob([readFileSync(path)]), basename2(path));
|
|
843
|
+
const res = await fetchFn(`${API_BASE}/media/upload/binary`, { method: "POST", headers, body: form });
|
|
844
|
+
const j = await res.json();
|
|
845
|
+
const url = j.data?.download_url;
|
|
846
|
+
if (url === void 0) throw new Error("cinematic: media upload failed");
|
|
847
|
+
return url;
|
|
848
|
+
};
|
|
849
|
+
const t2i = async (prompt) => poll(await post(MODEL_T2I, { prompt, size: T2I_SIZE }));
|
|
850
|
+
const edit = async (prompt, image) => poll(await post(MODEL_EDIT, { prompt, image }));
|
|
851
|
+
const kling = async (first, last, motion) => poll(
|
|
852
|
+
await post(MODEL_I2V, {
|
|
853
|
+
image: first,
|
|
854
|
+
last_image: last,
|
|
855
|
+
prompt: motion,
|
|
856
|
+
duration: CLIP_SECONDS,
|
|
857
|
+
negative_prompt: NEGATIVE_PROMPT
|
|
858
|
+
})
|
|
859
|
+
);
|
|
860
|
+
const tts = async (text) => poll(
|
|
861
|
+
await post(MODEL_TTS, {
|
|
862
|
+
text,
|
|
863
|
+
voice_id: voiceId,
|
|
864
|
+
stability: 0.3,
|
|
865
|
+
similarity: 0.75,
|
|
866
|
+
use_speaker_boost: true
|
|
867
|
+
}),
|
|
868
|
+
60,
|
|
869
|
+
2500
|
|
870
|
+
);
|
|
871
|
+
const beats = deriveBeats(scenes);
|
|
872
|
+
if (beats.length < 2) throw new Error("cinematic: need at least 2 scenes to build a film");
|
|
873
|
+
log("cinematic: hero + face ref...");
|
|
874
|
+
const { hero: heroPrompt, face: facePrompt } = deriveReferencePrompts();
|
|
875
|
+
const [heroUrl, faceUrl] = await Promise.all([t2i(heroPrompt), t2i(facePrompt)]);
|
|
876
|
+
log(`cinematic: ${beats.length} keyframes (parallel)...`);
|
|
877
|
+
const kfUrls = await Promise.all(beats.map((b) => edit(b.keyframe, heroUrl)));
|
|
878
|
+
const lastKfPath = join(workDir, "end.jpg");
|
|
879
|
+
await download(kfUrls[kfUrls.length - 1], lastKfPath);
|
|
880
|
+
const clipCount = beats.length - 1;
|
|
881
|
+
log(`cinematic: ${clipCount} Kling clips (parallel, first+last frame chained)...`);
|
|
882
|
+
const clipPaths = await Promise.all(
|
|
883
|
+
beats.slice(0, -1).map(async (b, i) => {
|
|
884
|
+
const path = join(workDir, `c${i}.mp4`);
|
|
885
|
+
try {
|
|
886
|
+
await download(await kling(kfUrls[i], kfUrls[i + 1], b.motion), path);
|
|
887
|
+
log(`cinematic: clip ${i}`);
|
|
888
|
+
return path;
|
|
889
|
+
} catch (err) {
|
|
890
|
+
log(`cinematic: clip ${i} failed (${err instanceof Error ? err.message : String(err)}) \u2014 skipping`);
|
|
891
|
+
return null;
|
|
892
|
+
}
|
|
893
|
+
})
|
|
894
|
+
);
|
|
895
|
+
const clips = clipPaths.filter((p) => p !== null && existsSync(p));
|
|
896
|
+
if (clips.length === 0) throw new Error("cinematic: all Kling clips failed");
|
|
897
|
+
const segs = [];
|
|
898
|
+
for (let i = 0; i < clips.length; i++) {
|
|
899
|
+
const seg = join(workDir, `s${i}.mp4`);
|
|
900
|
+
execFn("ffmpeg", [
|
|
901
|
+
"-y",
|
|
902
|
+
"-i",
|
|
903
|
+
clips[i],
|
|
904
|
+
"-an",
|
|
905
|
+
"-vf",
|
|
906
|
+
"scale=1280:720:force_original_aspect_ratio=increase,crop=1280:720,setsar=1,fps=24",
|
|
907
|
+
"-c:v",
|
|
908
|
+
"libx264",
|
|
909
|
+
"-pix_fmt",
|
|
910
|
+
"yuv420p",
|
|
911
|
+
seg
|
|
912
|
+
]);
|
|
913
|
+
segs.push(seg);
|
|
914
|
+
}
|
|
915
|
+
const endCard = join(workDir, "end.mp4");
|
|
916
|
+
execFn("ffmpeg", [
|
|
917
|
+
"-y",
|
|
918
|
+
"-i",
|
|
919
|
+
lastKfPath,
|
|
920
|
+
"-vf",
|
|
921
|
+
`zoompan=z='min(zoom+0.0008,1.06)':d=${CLIP_SECONDS * 24}:x='iw/2-(iw/zoom/2)':y='ih/2-(ih/zoom/2)':s=1280x720:fps=24,setsar=1`,
|
|
922
|
+
"-an",
|
|
923
|
+
"-c:v",
|
|
924
|
+
"libx264",
|
|
925
|
+
"-pix_fmt",
|
|
926
|
+
"yuv420p",
|
|
927
|
+
endCard
|
|
928
|
+
]);
|
|
929
|
+
segs.push(endCard);
|
|
930
|
+
const listPath = join(workDir, "list.txt");
|
|
931
|
+
writeFileSync(listPath, segs.map((s) => `file '${s.replace(/'/g, `'\\''`)}'`).join("\n"));
|
|
932
|
+
const baseRaw = join(workDir, "base_raw.mp4");
|
|
933
|
+
execFn("ffmpeg", ["-y", "-f", "concat", "-safe", "0", "-i", listPath, "-an", baseRaw]);
|
|
934
|
+
log(`cinematic: base cut assembled (${segs.length} segments)`);
|
|
935
|
+
let base = join(workDir, "base.mp4");
|
|
936
|
+
try {
|
|
937
|
+
log("cinematic: face-swap identity lock...");
|
|
938
|
+
const swapped = await poll(
|
|
939
|
+
await post(MODEL_FACE_SWAP, { video: await upload(baseRaw), face_image: faceUrl, target_gender: "female" }),
|
|
940
|
+
140
|
|
941
|
+
);
|
|
942
|
+
await download(swapped, base);
|
|
943
|
+
} catch (err) {
|
|
944
|
+
log(`cinematic: face-swap failed (${err instanceof Error ? err.message : String(err)}) \u2014 using the raw cut`);
|
|
945
|
+
copyFileSync(baseRaw, base);
|
|
946
|
+
}
|
|
947
|
+
const graded = join(workDir, "base_g.mp4");
|
|
948
|
+
execFn("ffmpeg", [
|
|
949
|
+
"-y",
|
|
950
|
+
"-i",
|
|
951
|
+
base,
|
|
952
|
+
"-vf",
|
|
953
|
+
"eq=contrast=1.06:saturation=1.08:brightness=0.01,colorbalance=rs=0.05:gs=0.01:bs=-0.04:rm=0.03:bm=-0.02,setsar=1",
|
|
954
|
+
"-an",
|
|
955
|
+
"-c:v",
|
|
956
|
+
"libx264",
|
|
957
|
+
"-pix_fmt",
|
|
958
|
+
"yuv420p",
|
|
959
|
+
graded
|
|
960
|
+
]);
|
|
961
|
+
log(`cinematic: ${beats.length} VO lines (parallel)...`);
|
|
962
|
+
const voPaths = await Promise.all(
|
|
963
|
+
beats.map(async (b, i) => {
|
|
964
|
+
const raw = join(workDir, `raw${i}.mp3`);
|
|
965
|
+
await download(await tts(b.vo), raw);
|
|
966
|
+
const processed = join(workDir, `r${i}.mp3`);
|
|
967
|
+
execFn("ffmpeg", [
|
|
968
|
+
"-y",
|
|
969
|
+
"-i",
|
|
970
|
+
raw,
|
|
971
|
+
"-af",
|
|
972
|
+
"highpass=f=80,acompressor=threshold=-18dB:ratio=2.5,volume=2dB",
|
|
973
|
+
processed
|
|
974
|
+
]);
|
|
975
|
+
log(`cinematic: line ${i} (${voiceId})`);
|
|
976
|
+
return processed;
|
|
977
|
+
})
|
|
978
|
+
);
|
|
979
|
+
const dur = beats.length * CLIP_SECONDS;
|
|
980
|
+
const inputs = ["-i", graded];
|
|
981
|
+
for (const p of voPaths) inputs.push("-i", p);
|
|
982
|
+
inputs.push("-f", "lavfi", "-i", `anoisesrc=color=pink:d=${dur}:a=0.025`);
|
|
983
|
+
const hissIdx = 1 + voPaths.length;
|
|
984
|
+
let fc = "";
|
|
985
|
+
voPaths.forEach((_, i) => {
|
|
986
|
+
const d = i * CLIP_SECONDS * 1e3;
|
|
987
|
+
fc += `[${i + 1}:a]adelay=${d}|${d}[v${i}];`;
|
|
988
|
+
});
|
|
989
|
+
fc += `[${hissIdx}:a]highpass=f=300,lowpass=f=3000,volume=0.5[hiss];`;
|
|
990
|
+
fc += voPaths.map((_, i) => `[v${i}]`).join("") + `[hiss]amix=inputs=${voPaths.length + 1}:duration=longest:dropout_transition=0,volume=1.3[a]`;
|
|
991
|
+
execFn("ffmpeg", [
|
|
992
|
+
"-y",
|
|
993
|
+
...inputs,
|
|
994
|
+
"-filter_complex",
|
|
995
|
+
fc,
|
|
996
|
+
"-map",
|
|
997
|
+
"0:v",
|
|
998
|
+
"-map",
|
|
999
|
+
"[a]",
|
|
1000
|
+
"-c:v",
|
|
1001
|
+
"copy",
|
|
1002
|
+
"-c:a",
|
|
1003
|
+
"aac",
|
|
1004
|
+
"-shortest",
|
|
1005
|
+
opts.out
|
|
1006
|
+
]);
|
|
1007
|
+
log(`cinematic: DONE -> ${opts.out}`);
|
|
1008
|
+
return { path: opts.out, beats };
|
|
1009
|
+
}
|
|
1010
|
+
|
|
690
1011
|
// src/index.ts
|
|
691
1012
|
function createHyperframesRunner() {
|
|
692
1013
|
return {
|
|
@@ -700,6 +1021,30 @@ function createHyperframesRunner() {
|
|
|
700
1021
|
}
|
|
701
1022
|
async function renderMovie(events, opts = {}) {
|
|
702
1023
|
const scenes = buildScenes(events, opts);
|
|
1024
|
+
const log = opts.log ?? ((msg) => process.stderr.write(`${msg}
|
|
1025
|
+
`));
|
|
1026
|
+
const sink = opts.notify ?? vibeCoreNotify;
|
|
1027
|
+
let out = opts.out;
|
|
1028
|
+
if ((opts.engine ?? "hyperframes") === "cinematic") {
|
|
1029
|
+
if (cinematicAvailable() && ffmpegAvailable()) {
|
|
1030
|
+
const cinOut = out ?? "./vibe-recap.mp4";
|
|
1031
|
+
const cinOpts = { out: cinOut, log };
|
|
1032
|
+
if (opts.apiKey !== void 0) cinOpts.apiKey = opts.apiKey;
|
|
1033
|
+
const cin = await renderCinematic(scenes, cinOpts);
|
|
1034
|
+
const result2 = { engine: "cinematic", path: cin.path };
|
|
1035
|
+
notifyRenderDone(sink, result2);
|
|
1036
|
+
return result2;
|
|
1037
|
+
}
|
|
1038
|
+
const why = !cinematicAvailable() ? "WAVESPEED_API_KEY is not set" : "ffmpeg is not on PATH";
|
|
1039
|
+
log(`vibemovie: cinematic engine unavailable (${why}) \u2014 falling back to hyperframes (offline, zero keys)`);
|
|
1040
|
+
if (out !== void 0) {
|
|
1041
|
+
const htmlOut = out.replace(/\.(mp4|mov|webm|mkv)$/i, ".html");
|
|
1042
|
+
if (htmlOut !== out) {
|
|
1043
|
+
log(`vibemovie: writing the hyperframes HTML to ${htmlOut}`);
|
|
1044
|
+
out = htmlOut;
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
703
1048
|
const cascade = createCascade({
|
|
704
1049
|
consent: createConsentLedger(),
|
|
705
1050
|
pickLocal: (capability) => Promise.resolve(capability === "video" ? createHyperframesRunner() : null)
|
|
@@ -713,11 +1058,26 @@ async function renderMovie(events, opts = {}) {
|
|
|
713
1058
|
if (opts.ratio !== void 0) req.ratio = opts.ratio;
|
|
714
1059
|
if (opts.title !== void 0) req.title = opts.title;
|
|
715
1060
|
const html = await provider.generate(req);
|
|
716
|
-
if (
|
|
717
|
-
await writeFile(
|
|
718
|
-
|
|
1061
|
+
if (out !== void 0) {
|
|
1062
|
+
await writeFile(out, html, "utf8");
|
|
1063
|
+
const result2 = { engine: "hyperframes", html, path: out };
|
|
1064
|
+
notifyRenderDone(sink, result2);
|
|
1065
|
+
return result2;
|
|
1066
|
+
}
|
|
1067
|
+
const result = { engine: "hyperframes", html };
|
|
1068
|
+
notifyRenderDone(sink, result);
|
|
1069
|
+
return result;
|
|
1070
|
+
}
|
|
1071
|
+
function notifyRenderDone(sink, result) {
|
|
1072
|
+
try {
|
|
1073
|
+
sink(
|
|
1074
|
+
makeEvent("render-done", "vibemovie", process.cwd(), {
|
|
1075
|
+
summary: `rendered ${result.engine} recap${result.path !== void 0 ? ` \u2192 ${result.path}` : ""}`,
|
|
1076
|
+
outputPath: result.path ?? null
|
|
1077
|
+
})
|
|
1078
|
+
);
|
|
1079
|
+
} catch {
|
|
719
1080
|
}
|
|
720
|
-
return { html };
|
|
721
1081
|
}
|
|
722
1082
|
|
|
723
1083
|
export {
|
|
@@ -725,5 +1085,10 @@ export {
|
|
|
725
1085
|
TEMPLATES,
|
|
726
1086
|
buildScenes,
|
|
727
1087
|
renderHyperframes,
|
|
1088
|
+
ENGINES,
|
|
1089
|
+
deriveReferencePrompts,
|
|
1090
|
+
deriveBeats,
|
|
1091
|
+
cinematicAvailable,
|
|
1092
|
+
renderCinematic,
|
|
728
1093
|
renderMovie
|
|
729
1094
|
};
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import { VibeEvent } from '@pooriaarab/vibe-core';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* scenes.ts — pure events → scene-list compiler.
|
|
5
|
+
*
|
|
6
|
+
* Takes normalized `VibeEvent`s (from vibe-core) or loose `RawEvent`s (anything a
|
|
7
|
+
* harness/logger might emit) and compiles them into an ordered, fully-described
|
|
8
|
+
* scene list for the Hyperframes renderer. No IO, no randomness, no clock — the
|
|
9
|
+
* same events always produce the same scenes, which makes this unit-testable and
|
|
10
|
+
* the rendered HTML deterministic.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
type Ratio = '16:9' | '9:16' | '1:1';
|
|
14
|
+
type Template = 'documentary' | 'speedrun' | 'meme';
|
|
15
|
+
type Transition = 'fade' | 'slide' | 'scale' | 'rise';
|
|
16
|
+
type SceneKind = 'title' | 'tasks' | 'diff' | 'terminal' | 'merge' | 'end';
|
|
17
|
+
/**
|
|
18
|
+
* A loosely-shaped event from any source: a harness transcript export, a JSON
|
|
19
|
+
* log, a hand-written fixture. Any field may be missing; known aliases
|
|
20
|
+
* (`type`/`timestamp`) are normalized.
|
|
21
|
+
*/
|
|
22
|
+
interface RawEvent {
|
|
23
|
+
kind?: string;
|
|
24
|
+
type?: string;
|
|
25
|
+
ts?: number;
|
|
26
|
+
timestamp?: number | string;
|
|
27
|
+
agent?: string;
|
|
28
|
+
cwd?: string;
|
|
29
|
+
payload?: Record<string, unknown>;
|
|
30
|
+
}
|
|
31
|
+
interface BuildOptions {
|
|
32
|
+
/** Accepted for API symmetry with the renderer; layout is a render-time concern. */
|
|
33
|
+
ratio?: Ratio;
|
|
34
|
+
/** Caption tone + pacing. Default 'documentary'. */
|
|
35
|
+
template?: Template;
|
|
36
|
+
/** Session name for the title card. Default: basename of the events' cwd. */
|
|
37
|
+
title?: string;
|
|
38
|
+
}
|
|
39
|
+
interface TitleData {
|
|
40
|
+
sessionName: string;
|
|
41
|
+
/** Total session length in whole minutes, counted up during the title scene. */
|
|
42
|
+
totalMinutes: number;
|
|
43
|
+
subtitle: string;
|
|
44
|
+
}
|
|
45
|
+
interface TaskRow {
|
|
46
|
+
label: string;
|
|
47
|
+
durationMin?: number;
|
|
48
|
+
}
|
|
49
|
+
interface TasksData {
|
|
50
|
+
/** Real number of completed tasks (may exceed `tasks.length`). */
|
|
51
|
+
total: number;
|
|
52
|
+
/** Rows actually shown (capped at MAX_TASK_ROWS). */
|
|
53
|
+
tasks: TaskRow[];
|
|
54
|
+
}
|
|
55
|
+
interface DiffData {
|
|
56
|
+
filesChanged: number;
|
|
57
|
+
additions: number;
|
|
58
|
+
deletions: number;
|
|
59
|
+
/** Up to 4 representative file paths. */
|
|
60
|
+
files: string[];
|
|
61
|
+
}
|
|
62
|
+
interface TermLine {
|
|
63
|
+
text: string;
|
|
64
|
+
cls: 'ok' | '';
|
|
65
|
+
}
|
|
66
|
+
interface TerminalData {
|
|
67
|
+
lines: TermLine[];
|
|
68
|
+
}
|
|
69
|
+
interface MergeData {
|
|
70
|
+
pr: number | null;
|
|
71
|
+
branch: string;
|
|
72
|
+
reviewers: number | null;
|
|
73
|
+
merged: boolean;
|
|
74
|
+
}
|
|
75
|
+
interface EndData {
|
|
76
|
+
tagline: string;
|
|
77
|
+
}
|
|
78
|
+
interface SceneBase {
|
|
79
|
+
/** DOM id in the rendered page, `scene-<kind>`. */
|
|
80
|
+
id: string;
|
|
81
|
+
transition: Transition;
|
|
82
|
+
/** Milliseconds on the timeline. */
|
|
83
|
+
duration: number;
|
|
84
|
+
/** Narrator caption shown while the scene is live. */
|
|
85
|
+
caption: string;
|
|
86
|
+
}
|
|
87
|
+
type Scene = (SceneBase & {
|
|
88
|
+
kind: 'title';
|
|
89
|
+
data: TitleData;
|
|
90
|
+
}) | (SceneBase & {
|
|
91
|
+
kind: 'tasks';
|
|
92
|
+
data: TasksData;
|
|
93
|
+
}) | (SceneBase & {
|
|
94
|
+
kind: 'diff';
|
|
95
|
+
data: DiffData;
|
|
96
|
+
}) | (SceneBase & {
|
|
97
|
+
kind: 'terminal';
|
|
98
|
+
data: TerminalData;
|
|
99
|
+
}) | (SceneBase & {
|
|
100
|
+
kind: 'merge';
|
|
101
|
+
data: MergeData;
|
|
102
|
+
}) | (SceneBase & {
|
|
103
|
+
kind: 'end';
|
|
104
|
+
data: EndData;
|
|
105
|
+
});
|
|
106
|
+
/**
|
|
107
|
+
* Compile events into an ordered scene list.
|
|
108
|
+
*
|
|
109
|
+
* Always emits a title card and an end card; the middle scenes appear only when
|
|
110
|
+
* the events carry the data for them (tasks → task ticks, file/diff stats →
|
|
111
|
+
* animated diff, notable command events → terminal moment, PR event → merge
|
|
112
|
+
* celebration). Empty input yields `[title, end]`.
|
|
113
|
+
*
|
|
114
|
+
* PURE: no IO, no clock, no randomness.
|
|
115
|
+
*/
|
|
116
|
+
declare function buildScenes(events: readonly (VibeEvent | RawEvent)[], opts?: BuildOptions): Scene[];
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* cinematic.ts — the BYO-key gen-video engine: session scenes → a real mp4.
|
|
120
|
+
*
|
|
121
|
+
* Opt-in tier above Hyperframes. Given the scene list from `buildScenes`, it
|
|
122
|
+
* renders a short cinematic character video of the session via wavespeed.ai
|
|
123
|
+
* and muxes an ElevenLabs voiceover with ffmpeg:
|
|
124
|
+
*
|
|
125
|
+
* 1. derive one beat per scene (~6): keyframe prompt + motion prompt + VO line
|
|
126
|
+
* 2. `bytedance/seedream-v4` — wide hero + crisp face headshot (parallel)
|
|
127
|
+
* 3. `wavespeed-ai/flux-kontext-max` — one keyframe per beat, image-conditioned
|
|
128
|
+
* on the hero with a LOCK string for character/set consistency (parallel)
|
|
129
|
+
* 4. `kwaivgi/kling-v2.5-turbo-pro/image-to-video` — chained 5s clips:
|
|
130
|
+
* `image` = keyframe N, `last_image` = keyframe N+1, so cuts are seamless
|
|
131
|
+
* 5. `wavespeed-ai/video-face-swap` — swap the headshot face onto the silent
|
|
132
|
+
* cut in one pass (identity lock), then one unified ffmpeg color grade
|
|
133
|
+
* 6. `elevenlabs/eleven-v3` — one VO line per beat, each delayed to its beat
|
|
134
|
+
* (`adelay=i*5000`), mixed over a near-silent pink-noise room-tone bed
|
|
135
|
+
*
|
|
136
|
+
* The pipeline (model IDs, params, poll loop, ffmpeg filters) follows the
|
|
137
|
+
* validated recipe in `launch-video-generation` ("Validated pipeline: cinematic
|
|
138
|
+
* character video") and the reference scripts it was distilled from.
|
|
139
|
+
*
|
|
140
|
+
* Requirements: `WAVESPEED_API_KEY` in the environment (ElevenLabs is reached
|
|
141
|
+
* through wavespeed, so one key covers both), `ffmpeg` on PATH, node >= 18
|
|
142
|
+
* (global fetch). Nothing is sent out without a key — callers should check
|
|
143
|
+
* `cinematicAvailable()` and fall back to Hyperframes when it returns false.
|
|
144
|
+
*/
|
|
145
|
+
|
|
146
|
+
type Engine = 'hyperframes' | 'cinematic';
|
|
147
|
+
declare const ENGINES: readonly Engine[];
|
|
148
|
+
/** ------------------------------------------------------------------ */
|
|
149
|
+
/** Beat derivation (pure — no IO, no clock, no randomness) */
|
|
150
|
+
/** ------------------------------------------------------------------ */
|
|
151
|
+
/**
|
|
152
|
+
* One narrated shot of the film. `keyframe` and `motion` are complete prompts
|
|
153
|
+
* (LOCK/STYLE baked in) for the image-edit and image-to-video models; `vo` is
|
|
154
|
+
* the eleven-v3 line for the beat (may carry `[emotion]` tags).
|
|
155
|
+
*/
|
|
156
|
+
interface Beat {
|
|
157
|
+
kind: SceneKind;
|
|
158
|
+
/** flux-kontext-max edit prompt: LOCK + shot description + STYLE. */
|
|
159
|
+
keyframe: string;
|
|
160
|
+
/** Kling motion prompt: action + STYLE. Unused on the last beat (end card). */
|
|
161
|
+
motion: string;
|
|
162
|
+
/** eleven-v3 narration line, played at this beat's 5s mark. */
|
|
163
|
+
vo: string;
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* The two seedream-v4 reference prompts: a wide hero that fixes look/wardrobe/set,
|
|
167
|
+
* and a crisp front-facing headshot used only as the face-swap anchor later.
|
|
168
|
+
*/
|
|
169
|
+
declare function deriveReferencePrompts(): {
|
|
170
|
+
hero: string;
|
|
171
|
+
face: string;
|
|
172
|
+
};
|
|
173
|
+
/**
|
|
174
|
+
* Compile the scene list into the shot list: one beat per scene, in scene order
|
|
175
|
+
* (title … end). A full session yields 6 beats; an empty one yields 2 (title +
|
|
176
|
+
* end cards). PURE: same scenes → same prompts.
|
|
177
|
+
*/
|
|
178
|
+
declare function deriveBeats(scenes: readonly Scene[]): Beat[];
|
|
179
|
+
/** ------------------------------------------------------------------ */
|
|
180
|
+
/** Availability */
|
|
181
|
+
/** ------------------------------------------------------------------ */
|
|
182
|
+
/** True when a usable wavespeed key is present — the only egress gate. */
|
|
183
|
+
declare function cinematicAvailable(env?: NodeJS.ProcessEnv): boolean;
|
|
184
|
+
type ExecFn = (file: string, args: readonly string[]) => void;
|
|
185
|
+
/** Injectable side-effects so tests can run the whole pipeline without network/ffmpeg. */
|
|
186
|
+
interface CinematicDeps {
|
|
187
|
+
fetchFn?: typeof fetch;
|
|
188
|
+
execFn?: ExecFn;
|
|
189
|
+
sleepFn?: (ms: number) => Promise<void>;
|
|
190
|
+
}
|
|
191
|
+
interface CinematicOptions {
|
|
192
|
+
/** Where the final mp4 is written. */
|
|
193
|
+
out: string;
|
|
194
|
+
/** wavespeed key. Default: `WAVESPEED_API_KEY` from the environment. */
|
|
195
|
+
apiKey?: string;
|
|
196
|
+
/** Scratch dir for keyframes/clips/mix parts. Default: a fresh mkdtemp. */
|
|
197
|
+
workDir?: string;
|
|
198
|
+
/** eleven-v3 voice. Default 'George'. */
|
|
199
|
+
voiceId?: string;
|
|
200
|
+
/** Progress lines. Default: stderr. */
|
|
201
|
+
log?: (msg: string) => void;
|
|
202
|
+
/** Test seam: override fetch/ffmpeg/sleep. */
|
|
203
|
+
deps?: CinematicDeps;
|
|
204
|
+
}
|
|
205
|
+
interface CinematicResult {
|
|
206
|
+
/** The rendered mp4 (`opts.out`). */
|
|
207
|
+
path: string;
|
|
208
|
+
/** The shot list the film was built from. */
|
|
209
|
+
beats: Beat[];
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Render the scene list as a gen-video mp4. Egresses to wavespeed.ai — the
|
|
213
|
+
* caller is responsible for the consent gate (`cinematicAvailable`) and the
|
|
214
|
+
* Hyperframes fallback. Throws on missing key, failed predictions, or ffmpeg
|
|
215
|
+
* errors; a failed face-swap degrades to the un-swapped cut (logged, not fatal).
|
|
216
|
+
*/
|
|
217
|
+
declare function renderCinematic(scenes: readonly Scene[], opts: CinematicOptions): Promise<CinematicResult>;
|
|
218
|
+
|
|
219
|
+
export { type BuildOptions as B, type CinematicDeps as C, type DiffData as D, type Engine as E, type MergeData as M, type Ratio as R, type Scene as S, type Template as T, type RawEvent as a, type Beat as b, type CinematicOptions as c, type CinematicResult as d, ENGINES as e, type EndData as f, type SceneKind as g, type TaskRow as h, type TasksData as i, type TermLine as j, type TerminalData as k, type TitleData as l, type Transition as m, buildScenes as n, cinematicAvailable as o, deriveBeats as p, deriveReferencePrompts as q, renderCinematic as r };
|
package/dist/cli.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { R as Ratio, T as Template } from './
|
|
2
|
+
import { R as Ratio, T as Template, E as Engine } from './cinematic-C31_2Fw6.js';
|
|
3
3
|
import '@pooriaarab/vibe-core';
|
|
4
4
|
|
|
5
5
|
/**
|
|
@@ -13,12 +13,14 @@ declare const VERSION = "0.1.0";
|
|
|
13
13
|
* vibemovie CLI — render a session recap from JSON events, or serve MCP.
|
|
14
14
|
*
|
|
15
15
|
* vibemovie render [file.json] [--ratio 16:9|9:16|1:1] [--template documentary|speedrun|meme]
|
|
16
|
-
* [--out recap.html] [--title "my session"]
|
|
16
|
+
* [--engine hyperframes|cinematic] [--out recap.html] [--title "my session"]
|
|
17
17
|
* vibemovie mcp start the MCP server on stdio
|
|
18
18
|
* vibemovie --version · vibemovie --help
|
|
19
19
|
*
|
|
20
|
-
* Events are read from the file argument, or stdin when piped. The
|
|
21
|
-
*
|
|
20
|
+
* Events are read from the file argument, or stdin when piped. The default
|
|
21
|
+
* engine is the local Hyperframes tier — offline, zero keys. The cinematic
|
|
22
|
+
* engine is opt-in and BYO-key: it needs WAVESPEED_API_KEY and ffmpeg, and
|
|
23
|
+
* falls back to Hyperframes when either is missing.
|
|
22
24
|
*/
|
|
23
25
|
|
|
24
26
|
declare class CliError extends Error {
|
|
@@ -29,6 +31,7 @@ interface CliArgs {
|
|
|
29
31
|
file?: string;
|
|
30
32
|
ratio: Ratio;
|
|
31
33
|
template: Template;
|
|
34
|
+
engine: Engine;
|
|
32
35
|
out: string;
|
|
33
36
|
title?: string;
|
|
34
37
|
}
|
package/dist/cli.js
CHANGED
|
@@ -3,14 +3,15 @@ import {
|
|
|
3
3
|
VERSION
|
|
4
4
|
} from "./chunk-HI6AAJID.js";
|
|
5
5
|
import {
|
|
6
|
+
ENGINES,
|
|
6
7
|
RATIOS,
|
|
7
8
|
TEMPLATES,
|
|
8
9
|
renderMovie
|
|
9
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-DO2IWHL6.js";
|
|
10
11
|
|
|
11
12
|
// src/cli.ts
|
|
12
13
|
import { readFile } from "fs/promises";
|
|
13
|
-
import
|
|
14
|
+
import "url";
|
|
14
15
|
var CliError = class extends Error {
|
|
15
16
|
};
|
|
16
17
|
var HELP = `vibemovie \u2014 your agent coding session as a short recap video
|
|
@@ -24,13 +25,19 @@ Usage:
|
|
|
24
25
|
Options:
|
|
25
26
|
--ratio 16:9|9:16|1:1 player aspect ratio (default 16:9)
|
|
26
27
|
--template documentary|speedrun|meme caption tone + pacing (default documentary)
|
|
27
|
-
--
|
|
28
|
+
--engine hyperframes|cinematic render engine (default hyperframes)
|
|
29
|
+
--out <path> output file (default ./vibe-recap.html,
|
|
30
|
+
or ./vibe-recap.mp4 for cinematic)
|
|
28
31
|
--title <name> session name on the title card
|
|
29
32
|
|
|
30
33
|
Input: a JSON array of events (or { "events": [...] }). Each event:
|
|
31
34
|
{ "kind": "task-done", "ts": 1720000000000, "payload": { "label": "..." } }
|
|
32
35
|
|
|
33
|
-
|
|
36
|
+
Engines:
|
|
37
|
+
hyperframes renders on-device: offline, zero keys, no data out.
|
|
38
|
+
cinematic real gen-video mp4 via wavespeed.ai (BYO key) + ElevenLabs VO.
|
|
39
|
+
Needs WAVESPEED_API_KEY in the env and ffmpeg on PATH; falls
|
|
40
|
+
back to hyperframes when either is missing.
|
|
34
41
|
`;
|
|
35
42
|
function takeValue(argv, i, flag, inline) {
|
|
36
43
|
if (inline !== void 0) {
|
|
@@ -43,11 +50,12 @@ function takeValue(argv, i, flag, inline) {
|
|
|
43
50
|
}
|
|
44
51
|
function parseArgs(argv) {
|
|
45
52
|
if (argv.length === 0) {
|
|
46
|
-
return { command: "help", ratio: "16:9", template: "documentary", out: "./vibe-recap.html" };
|
|
53
|
+
return { command: "help", ratio: "16:9", template: "documentary", engine: "hyperframes", out: "./vibe-recap.html" };
|
|
47
54
|
}
|
|
48
|
-
const args = { command: "render", ratio: "16:9", template: "documentary", out: "./vibe-recap.html" };
|
|
55
|
+
const args = { command: "render", ratio: "16:9", template: "documentary", engine: "hyperframes", out: "./vibe-recap.html" };
|
|
49
56
|
let sawCommand = false;
|
|
50
57
|
let sawFile = false;
|
|
58
|
+
let sawOut = false;
|
|
51
59
|
for (let i = 0; i < argv.length; i++) {
|
|
52
60
|
const tok = argv[i];
|
|
53
61
|
if (tok === "--help" || tok === "-h") {
|
|
@@ -74,7 +82,7 @@ function parseArgs(argv) {
|
|
|
74
82
|
if (tok.startsWith("--")) {
|
|
75
83
|
const eq = tok.indexOf("=");
|
|
76
84
|
const flag = eq === -1 ? tok : tok.slice(0, eq);
|
|
77
|
-
if (flag !== "--ratio" && flag !== "--template" && flag !== "--out" && flag !== "--title") {
|
|
85
|
+
if (flag !== "--ratio" && flag !== "--template" && flag !== "--engine" && flag !== "--out" && flag !== "--title") {
|
|
78
86
|
throw new CliError(`unknown flag: ${flag}`);
|
|
79
87
|
}
|
|
80
88
|
const inline = eq === -1 ? void 0 : tok.slice(eq + 1);
|
|
@@ -90,8 +98,14 @@ function parseArgs(argv) {
|
|
|
90
98
|
throw new CliError(`invalid --template "${value}" (expected ${TEMPLATES.join("|")})`);
|
|
91
99
|
}
|
|
92
100
|
args.template = value;
|
|
101
|
+
} else if (flag === "--engine") {
|
|
102
|
+
if (!ENGINES.includes(value)) {
|
|
103
|
+
throw new CliError(`invalid --engine "${value}" (expected ${ENGINES.join("|")})`);
|
|
104
|
+
}
|
|
105
|
+
args.engine = value;
|
|
93
106
|
} else if (flag === "--out") {
|
|
94
107
|
args.out = value;
|
|
108
|
+
sawOut = true;
|
|
95
109
|
} else if (flag === "--title") {
|
|
96
110
|
args.title = value;
|
|
97
111
|
} else {
|
|
@@ -112,6 +126,9 @@ function parseArgs(argv) {
|
|
|
112
126
|
if (args.command === "mcp" && (args.file !== void 0 || sawFile)) {
|
|
113
127
|
throw new CliError("mcp takes no input file");
|
|
114
128
|
}
|
|
129
|
+
if (args.engine === "cinematic" && !sawOut) {
|
|
130
|
+
args.out = "./vibe-recap.mp4";
|
|
131
|
+
}
|
|
115
132
|
return args;
|
|
116
133
|
}
|
|
117
134
|
async function readStdin() {
|
|
@@ -168,14 +185,19 @@ async function main(argv) {
|
|
|
168
185
|
const result = await renderMovie(events, {
|
|
169
186
|
ratio: args.ratio,
|
|
170
187
|
template: args.template,
|
|
188
|
+
engine: args.engine,
|
|
171
189
|
out: args.out,
|
|
190
|
+
log: (msg) => process.stderr.write(`${msg}
|
|
191
|
+
`),
|
|
172
192
|
...args.title !== void 0 ? { title: args.title } : {}
|
|
173
193
|
});
|
|
174
194
|
process.stdout.write(`\u2713 recap rendered \u2192 ${result.path}
|
|
175
195
|
`);
|
|
176
|
-
process.stdout.write(
|
|
196
|
+
process.stdout.write(
|
|
197
|
+
result.engine === "cinematic" ? " cinematic \xB7 wavespeed gen-video + eleven-v3 narration \xB7 BYO key\n" : " hyperframes \xB7 offline \xB7 zero keys\n"
|
|
198
|
+
);
|
|
177
199
|
}
|
|
178
|
-
var invokedAsScript = typeof process.argv[1] === "string"
|
|
200
|
+
var invokedAsScript = typeof process.argv[1] === "string";
|
|
179
201
|
if (invokedAsScript) {
|
|
180
202
|
main(process.argv.slice(2)).catch((err) => {
|
|
181
203
|
const msg = err instanceof Error ? err.message : String(err);
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { R as Ratio, S as Scene, B as BuildOptions, a as RawEvent } from './
|
|
2
|
-
export { D as DiffData,
|
|
1
|
+
import { R as Ratio, S as Scene, B as BuildOptions, E as Engine, a as RawEvent } from './cinematic-C31_2Fw6.js';
|
|
2
|
+
export { b as Beat, C as CinematicDeps, c as CinematicOptions, d as CinematicResult, D as DiffData, e as ENGINES, f as EndData, M as MergeData, g as SceneKind, h as TaskRow, i as TasksData, T as Template, j as TermLine, k as TerminalData, l as TitleData, m as Transition, n as buildScenes, o as cinematicAvailable, p as deriveBeats, q as deriveReferencePrompts, r as renderCinematic } from './cinematic-C31_2Fw6.js';
|
|
3
3
|
import { VibeEvent } from '@pooriaarab/vibe-core';
|
|
4
4
|
export { VibeEvent } from '@pooriaarab/vibe-core';
|
|
5
5
|
|
|
@@ -31,38 +31,64 @@ declare function renderHyperframes(scenes: readonly Scene[], opts?: RenderOption
|
|
|
31
31
|
/**
|
|
32
32
|
* @pooriaarab/vibemovie — your agent coding session as a short recap video.
|
|
33
33
|
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
34
|
+
* Two render engines:
|
|
35
|
+
*
|
|
36
|
+
* - **hyperframes** (default) — a self-contained animated HTML page rendered
|
|
37
|
+
* 100% on-device: no gen-video model, no API key, no network.
|
|
38
|
+
* - **cinematic** (opt-in, BYO key) — a real gen-video mp4 via wavespeed.ai
|
|
39
|
+
* (Kling chained clips + face-swap identity lock + ElevenLabs VO). Requires
|
|
40
|
+
* `WAVESPEED_API_KEY` and ffmpeg; without them it falls back to Hyperframes
|
|
41
|
+
* with a clear message — it never fails hard and never egresses without a key.
|
|
39
42
|
*
|
|
40
43
|
* ```ts
|
|
41
44
|
* import { renderMovie } from '@pooriaarab/vibemovie';
|
|
42
45
|
* const { html, path } = await renderMovie(events, { ratio: '9:16', out: 'recap.html' });
|
|
46
|
+
* const video = await renderMovie(events, { engine: 'cinematic', out: 'recap.mp4' });
|
|
43
47
|
* ```
|
|
44
48
|
*/
|
|
45
49
|
|
|
50
|
+
/** Sink for milestone notifications. Injectable so tests can capture events. */
|
|
51
|
+
type NotifySink = (event: VibeEvent) => void;
|
|
46
52
|
interface RenderMovieOptions extends BuildOptions, RenderOptions {
|
|
47
|
-
/** When set, the
|
|
53
|
+
/** When set, the output is also written to this path (HTML or mp4, per engine). */
|
|
48
54
|
out?: string;
|
|
55
|
+
/** Render engine. Default 'hyperframes' (offline, zero keys). */
|
|
56
|
+
engine?: Engine;
|
|
57
|
+
/** wavespeed key for the cinematic engine. Default: `WAVESPEED_API_KEY` from env. */
|
|
58
|
+
apiKey?: string;
|
|
59
|
+
/** Status/fallback messages. Default: stderr. */
|
|
60
|
+
log?: (msg: string) => void;
|
|
61
|
+
/** Override the notification sink (tests). Defaults to vibe-core's `notify`. */
|
|
62
|
+
notify?: NotifySink;
|
|
49
63
|
}
|
|
50
64
|
interface RenderMovieResult {
|
|
51
|
-
/** The
|
|
52
|
-
|
|
53
|
-
/**
|
|
65
|
+
/** The engine that actually produced the output (cinematic can fall back). */
|
|
66
|
+
engine: Engine;
|
|
67
|
+
/** The self-contained recap HTML — present iff `engine` is 'hyperframes'. */
|
|
68
|
+
html?: string;
|
|
69
|
+
/** Path the output was written to — when `opts.out` was set (always for cinematic). */
|
|
54
70
|
path?: string;
|
|
55
71
|
}
|
|
56
72
|
/**
|
|
57
|
-
* Compile events → scenes → a
|
|
73
|
+
* Compile events → scenes → a recap via the selected engine.
|
|
74
|
+
*
|
|
75
|
+
* `hyperframes` (default) goes through the vibe-core cascade with egress
|
|
76
|
+
* disabled and an in-memory consent ledger, so it always resolves to the local
|
|
77
|
+
* Hyperframes runner — offline with zero keys. `cinematic` is opt-in egress:
|
|
78
|
+
* it renders only when a wavespeed key is present (and ffmpeg runs); otherwise
|
|
79
|
+
* it logs why and falls back to the Hyperframes tier — never a hard failure,
|
|
80
|
+
* never data out without a key.
|
|
58
81
|
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
82
|
+
* When `opts.out` is set the output is also written to disk. Cinematic output
|
|
83
|
+
* is always a file (default `./vibe-recap.mp4` when `out` is omitted); if the
|
|
84
|
+
* cinematic engine falls back, a video-looking `out` extension is rewritten to
|
|
85
|
+
* `.html` so the artifact matches its contents.
|
|
63
86
|
*
|
|
64
|
-
*
|
|
87
|
+
* A successful render (either engine, including a cinematic → hyperframes
|
|
88
|
+
* fallback) fires ONE best-effort `render-done` vibenotify event via the
|
|
89
|
+
* injectable sink (`opts.notify`, default vibe-core's `notify`). A failed
|
|
90
|
+
* render fires nothing.
|
|
65
91
|
*/
|
|
66
92
|
declare function renderMovie(events: readonly (VibeEvent | RawEvent)[], opts?: RenderMovieOptions): Promise<RenderMovieResult>;
|
|
67
93
|
|
|
68
|
-
export { BuildOptions, Ratio, RawEvent, type RenderMovieOptions, type RenderMovieResult, type RenderOptions, Scene, renderHyperframes, renderMovie };
|
|
94
|
+
export { BuildOptions, Engine, type NotifySink, Ratio, RawEvent, type RenderMovieOptions, type RenderMovieResult, type RenderOptions, Scene, renderHyperframes, renderMovie };
|
package/dist/index.js
CHANGED
|
@@ -1,10 +1,20 @@
|
|
|
1
1
|
import {
|
|
2
|
+
ENGINES,
|
|
2
3
|
buildScenes,
|
|
4
|
+
cinematicAvailable,
|
|
5
|
+
deriveBeats,
|
|
6
|
+
deriveReferencePrompts,
|
|
7
|
+
renderCinematic,
|
|
3
8
|
renderHyperframes,
|
|
4
9
|
renderMovie
|
|
5
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-DO2IWHL6.js";
|
|
6
11
|
export {
|
|
12
|
+
ENGINES,
|
|
7
13
|
buildScenes,
|
|
14
|
+
cinematicAvailable,
|
|
15
|
+
deriveBeats,
|
|
16
|
+
deriveReferencePrompts,
|
|
17
|
+
renderCinematic,
|
|
8
18
|
renderHyperframes,
|
|
9
19
|
renderMovie
|
|
10
20
|
};
|
package/dist/mcp.d.ts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* vibemovie MCP server (stdio) — lets an agent render a session recap.
|
|
3
3
|
*
|
|
4
|
-
* Exposes one tool, `render`: { events, ratio?, template?, out? } →
|
|
5
|
-
* HTML (or the output path when `out` is given).
|
|
6
|
-
* Hyperframes tier, so the tool works offline with zero keys
|
|
4
|
+
* Exposes one tool, `render`: { events, ratio?, template?, engine?, out? } →
|
|
5
|
+
* the recap HTML (or the output path when `out` is given). The default engine
|
|
6
|
+
* is the local Hyperframes tier, so the tool works offline with zero keys;
|
|
7
|
+
* `engine: 'cinematic'` opts into the BYO-key wavespeed gen-video pipeline
|
|
8
|
+
* (falls back to Hyperframes when no key/ffmpeg is available).
|
|
7
9
|
*
|
|
8
10
|
* Uses the SDK's low-level Server with a plain JSON Schema for input — no
|
|
9
11
|
* schema-library dependency beyond the SDK itself.
|
package/dist/mcp.js
CHANGED
|
@@ -2,10 +2,11 @@ import {
|
|
|
2
2
|
VERSION
|
|
3
3
|
} from "./chunk-HI6AAJID.js";
|
|
4
4
|
import {
|
|
5
|
+
ENGINES,
|
|
5
6
|
RATIOS,
|
|
6
7
|
TEMPLATES,
|
|
7
8
|
renderMovie
|
|
8
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-DO2IWHL6.js";
|
|
9
10
|
|
|
10
11
|
// src/mcp.ts
|
|
11
12
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
@@ -14,7 +15,7 @@ import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprot
|
|
|
14
15
|
import { pathToFileURL } from "url";
|
|
15
16
|
var RENDER_TOOL = {
|
|
16
17
|
name: "render",
|
|
17
|
-
description: 'Render an agent coding session as a recap "movie" \u2014 a self-contained animated HTML page (
|
|
18
|
+
description: 'Render an agent coding session as a recap "movie". Default engine is hyperframes \u2014 a self-contained animated HTML page (offline, zero keys, no data out). Set engine to "cinematic" for a real gen-video mp4 via wavespeed.ai (BYO key: needs WAVESPEED_API_KEY in the server env and ffmpeg on PATH; falls back to hyperframes when unavailable). Returns the HTML, or the output path when `out` is set.',
|
|
18
19
|
inputSchema: {
|
|
19
20
|
type: "object",
|
|
20
21
|
properties: {
|
|
@@ -25,8 +26,9 @@ var RENDER_TOOL = {
|
|
|
25
26
|
},
|
|
26
27
|
ratio: { type: "string", enum: [...RATIOS], description: "Player aspect ratio (default 16:9)." },
|
|
27
28
|
template: { type: "string", enum: [...TEMPLATES], description: "Caption tone + pacing (default documentary)." },
|
|
29
|
+
engine: { type: "string", enum: [...ENGINES], description: "Render engine (default hyperframes)." },
|
|
28
30
|
title: { type: "string", description: "Session name on the title card." },
|
|
29
|
-
out: { type: "string", description: "Optional path to write the HTML to." }
|
|
31
|
+
out: { type: "string", description: "Optional path to write the HTML (or mp4 for cinematic) to." }
|
|
30
32
|
},
|
|
31
33
|
required: ["events"]
|
|
32
34
|
}
|
|
@@ -51,6 +53,7 @@ async function startMcpServer() {
|
|
|
51
53
|
}
|
|
52
54
|
const ratio = enumArg(args["ratio"], "ratio", RATIOS);
|
|
53
55
|
const template = enumArg(args["template"], "template", TEMPLATES);
|
|
56
|
+
const engine = enumArg(args["engine"], "engine", ENGINES);
|
|
54
57
|
const title = args["title"];
|
|
55
58
|
const out = args["out"];
|
|
56
59
|
if (title !== void 0 && typeof title !== "string") throw new Error('render: "title" must be a string');
|
|
@@ -58,10 +61,15 @@ async function startMcpServer() {
|
|
|
58
61
|
const result = await renderMovie(args["events"], {
|
|
59
62
|
...ratio !== void 0 ? { ratio } : {},
|
|
60
63
|
...template !== void 0 ? { template } : {},
|
|
64
|
+
...engine !== void 0 ? { engine } : {},
|
|
61
65
|
...title !== void 0 ? { title } : {},
|
|
62
|
-
...out !== void 0 ? { out } : {}
|
|
66
|
+
...out !== void 0 ? { out } : {},
|
|
67
|
+
// stdio transport: anything user-facing must stay off stdout
|
|
68
|
+
log: (msg) => process.stderr.write(`${msg}
|
|
69
|
+
`)
|
|
63
70
|
});
|
|
64
|
-
const
|
|
71
|
+
const tier = result.engine === "cinematic" ? "cinematic \xB7 wavespeed gen-video + eleven-v3 narration \xB7 BYO key" : "hyperframes \xB7 offline \xB7 zero keys";
|
|
72
|
+
const text = result.path !== void 0 ? `vibemovie recap written to ${result.path} (${tier})` : result.html ?? "";
|
|
65
73
|
return { content: [{ type: "text", text }] };
|
|
66
74
|
});
|
|
67
75
|
await server.connect(new StdioServerTransport());
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vibemovie",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Your agent coding session as a short recap video — Hyperframes (HTML/CSS/JS) with zero gen-video keys, or your own video model. For Claude Code, Codex, Gemini, and other agentic CLIs. Local-first.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -26,8 +26,8 @@
|
|
|
26
26
|
"test:watch": "vitest"
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"@
|
|
30
|
-
"@
|
|
29
|
+
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
30
|
+
"@pooriaarab/vibe-core": "^0.2.0"
|
|
31
31
|
},
|
|
32
32
|
"devDependencies": {
|
|
33
33
|
"@types/node": "^20.11.0",
|
|
@@ -1,118 +0,0 @@
|
|
|
1
|
-
import { VibeEvent } from '@pooriaarab/vibe-core';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* scenes.ts — pure events → scene-list compiler.
|
|
5
|
-
*
|
|
6
|
-
* Takes normalized `VibeEvent`s (from vibe-core) or loose `RawEvent`s (anything a
|
|
7
|
-
* harness/logger might emit) and compiles them into an ordered, fully-described
|
|
8
|
-
* scene list for the Hyperframes renderer. No IO, no randomness, no clock — the
|
|
9
|
-
* same events always produce the same scenes, which makes this unit-testable and
|
|
10
|
-
* the rendered HTML deterministic.
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
type Ratio = '16:9' | '9:16' | '1:1';
|
|
14
|
-
type Template = 'documentary' | 'speedrun' | 'meme';
|
|
15
|
-
type Transition = 'fade' | 'slide' | 'scale' | 'rise';
|
|
16
|
-
type SceneKind = 'title' | 'tasks' | 'diff' | 'terminal' | 'merge' | 'end';
|
|
17
|
-
/**
|
|
18
|
-
* A loosely-shaped event from any source: a harness transcript export, a JSON
|
|
19
|
-
* log, a hand-written fixture. Any field may be missing; known aliases
|
|
20
|
-
* (`type`/`timestamp`) are normalized.
|
|
21
|
-
*/
|
|
22
|
-
interface RawEvent {
|
|
23
|
-
kind?: string;
|
|
24
|
-
type?: string;
|
|
25
|
-
ts?: number;
|
|
26
|
-
timestamp?: number | string;
|
|
27
|
-
agent?: string;
|
|
28
|
-
cwd?: string;
|
|
29
|
-
payload?: Record<string, unknown>;
|
|
30
|
-
}
|
|
31
|
-
interface BuildOptions {
|
|
32
|
-
/** Accepted for API symmetry with the renderer; layout is a render-time concern. */
|
|
33
|
-
ratio?: Ratio;
|
|
34
|
-
/** Caption tone + pacing. Default 'documentary'. */
|
|
35
|
-
template?: Template;
|
|
36
|
-
/** Session name for the title card. Default: basename of the events' cwd. */
|
|
37
|
-
title?: string;
|
|
38
|
-
}
|
|
39
|
-
interface TitleData {
|
|
40
|
-
sessionName: string;
|
|
41
|
-
/** Total session length in whole minutes, counted up during the title scene. */
|
|
42
|
-
totalMinutes: number;
|
|
43
|
-
subtitle: string;
|
|
44
|
-
}
|
|
45
|
-
interface TaskRow {
|
|
46
|
-
label: string;
|
|
47
|
-
durationMin?: number;
|
|
48
|
-
}
|
|
49
|
-
interface TasksData {
|
|
50
|
-
/** Real number of completed tasks (may exceed `tasks.length`). */
|
|
51
|
-
total: number;
|
|
52
|
-
/** Rows actually shown (capped at MAX_TASK_ROWS). */
|
|
53
|
-
tasks: TaskRow[];
|
|
54
|
-
}
|
|
55
|
-
interface DiffData {
|
|
56
|
-
filesChanged: number;
|
|
57
|
-
additions: number;
|
|
58
|
-
deletions: number;
|
|
59
|
-
/** Up to 4 representative file paths. */
|
|
60
|
-
files: string[];
|
|
61
|
-
}
|
|
62
|
-
interface TermLine {
|
|
63
|
-
text: string;
|
|
64
|
-
cls: 'ok' | '';
|
|
65
|
-
}
|
|
66
|
-
interface TerminalData {
|
|
67
|
-
lines: TermLine[];
|
|
68
|
-
}
|
|
69
|
-
interface MergeData {
|
|
70
|
-
pr: number | null;
|
|
71
|
-
branch: string;
|
|
72
|
-
reviewers: number | null;
|
|
73
|
-
merged: boolean;
|
|
74
|
-
}
|
|
75
|
-
interface EndData {
|
|
76
|
-
tagline: string;
|
|
77
|
-
}
|
|
78
|
-
interface SceneBase {
|
|
79
|
-
/** DOM id in the rendered page, `scene-<kind>`. */
|
|
80
|
-
id: string;
|
|
81
|
-
transition: Transition;
|
|
82
|
-
/** Milliseconds on the timeline. */
|
|
83
|
-
duration: number;
|
|
84
|
-
/** Narrator caption shown while the scene is live. */
|
|
85
|
-
caption: string;
|
|
86
|
-
}
|
|
87
|
-
type Scene = (SceneBase & {
|
|
88
|
-
kind: 'title';
|
|
89
|
-
data: TitleData;
|
|
90
|
-
}) | (SceneBase & {
|
|
91
|
-
kind: 'tasks';
|
|
92
|
-
data: TasksData;
|
|
93
|
-
}) | (SceneBase & {
|
|
94
|
-
kind: 'diff';
|
|
95
|
-
data: DiffData;
|
|
96
|
-
}) | (SceneBase & {
|
|
97
|
-
kind: 'terminal';
|
|
98
|
-
data: TerminalData;
|
|
99
|
-
}) | (SceneBase & {
|
|
100
|
-
kind: 'merge';
|
|
101
|
-
data: MergeData;
|
|
102
|
-
}) | (SceneBase & {
|
|
103
|
-
kind: 'end';
|
|
104
|
-
data: EndData;
|
|
105
|
-
});
|
|
106
|
-
/**
|
|
107
|
-
* Compile events into an ordered scene list.
|
|
108
|
-
*
|
|
109
|
-
* Always emits a title card and an end card; the middle scenes appear only when
|
|
110
|
-
* the events carry the data for them (tasks → task ticks, file/diff stats →
|
|
111
|
-
* animated diff, notable command events → terminal moment, PR event → merge
|
|
112
|
-
* celebration). Empty input yields `[title, end]`.
|
|
113
|
-
*
|
|
114
|
-
* PURE: no IO, no clock, no randomness.
|
|
115
|
-
*/
|
|
116
|
-
declare function buildScenes(events: readonly (VibeEvent | RawEvent)[], opts?: BuildOptions): Scene[];
|
|
117
|
-
|
|
118
|
-
export { type BuildOptions as B, type DiffData as D, type EndData as E, type MergeData as M, type Ratio as R, type Scene as S, type Template as T, type RawEvent as a, type SceneKind as b, type TaskRow as c, type TasksData as d, type TermLine as e, type TerminalData as f, type TitleData as g, type Transition as h, buildScenes as i };
|