@mevdragon/vidfarm-devcli 0.20.4 → 0.20.6
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/SKILL.director.md +64 -3
- package/demo/dist/app.js +50 -50
- package/dist/src/app.js +3 -2
- package/dist/src/cli.js +183 -3
- package/dist/src/editor-chat.js +75 -3
- package/dist/src/services/hyperframes.js +195 -27
- package/package.json +2 -2
|
@@ -1550,10 +1550,47 @@ Return strictly valid JSON matching this schema:
|
|
|
1550
1550
|
"arc": string,
|
|
1551
1551
|
"peak_moment": string,
|
|
1552
1552
|
"mechanism": string,
|
|
1553
|
+
"contrast": string,
|
|
1553
1554
|
"humor": string,
|
|
1555
|
+
"audio_visual_tension": string,
|
|
1554
1556
|
"delivery": string,
|
|
1555
1557
|
"preserve": [string, ...]
|
|
1556
1558
|
},
|
|
1559
|
+
"static_vs_pivot": {
|
|
1560
|
+
"summary": string,
|
|
1561
|
+
"scene_replacement": {
|
|
1562
|
+
"overall": "replace_most" | "replace_some" | "keep_most" | "keep_all",
|
|
1563
|
+
"reason": string,
|
|
1564
|
+
"must_keep": [string, ...],
|
|
1565
|
+
"should_replace": [string, ...],
|
|
1566
|
+
"load_bearing_scenes": [string, ...]
|
|
1567
|
+
},
|
|
1568
|
+
"narration": {
|
|
1569
|
+
"overall": "keep_original" | "customize_script" | "optional",
|
|
1570
|
+
"reason": string,
|
|
1571
|
+
"same_timing_required": boolean,
|
|
1572
|
+
"use_premium_tts_when_customizing": boolean
|
|
1573
|
+
},
|
|
1574
|
+
"music": {
|
|
1575
|
+
"overall": "keep_original" | "recreate_under_narration" | "optional" | "none",
|
|
1576
|
+
"reason": string,
|
|
1577
|
+
"volume_guidance": string
|
|
1578
|
+
},
|
|
1579
|
+
"captions": {
|
|
1580
|
+
"overall": "reuse_existing" | "retime_to_new_narration" | "optional" | "none",
|
|
1581
|
+
"reason": string,
|
|
1582
|
+
"animation_dependency": string
|
|
1583
|
+
},
|
|
1584
|
+
"visual_rebuild": {
|
|
1585
|
+
"default_strategy": "ai_images_with_ken_burns" | "real_footage" | "mixed",
|
|
1586
|
+
"reason": string
|
|
1587
|
+
},
|
|
1588
|
+
"viral_dna": {
|
|
1589
|
+
"must_keep": [string, ...],
|
|
1590
|
+
"flexible": [string, ...],
|
|
1591
|
+
"do_not_change": [string, ...]
|
|
1592
|
+
}
|
|
1593
|
+
},
|
|
1557
1594
|
"preserve": [string, ...],
|
|
1558
1595
|
"avoid": [string, ...],
|
|
1559
1596
|
"promotions": [string, ...],
|
|
@@ -1598,10 +1635,20 @@ VIRAL_DNA RULES:
|
|
|
1598
1635
|
- "arc": how the feeling MOVES across the runtime (e.g. "curiosity → mounting tension → relief-laugh", "flat setup → sudden absurd turn").
|
|
1599
1636
|
- "peak_moment": the single beat that lands the emotional hit — the punchline, the reveal, the gut-punch, the beat-drop — and roughly when.
|
|
1600
1637
|
- "mechanism": WHY it lands — the technique doing the work (comedic timing, irony, subversion of expectation, escalation, deadpan intonation, relatability, a trending-sound drop synced to the visual). Weigh the AUDIO here: on TikTok/Reels the trending sound or music bed is frequently the single biggest driver of the emotional hit, so if a recognizable trending sound, a beat drop, or a music swell is doing the work, name it as the mechanism.
|
|
1601
|
-
- "
|
|
1638
|
+
- "contrast": the specific mismatch / juxtaposition that creates the feeling. This is CRITICAL for irony- or sass-driven formats: identify the actual tension, not just the visible gesture. Examples: "hard, macho soundtrack over a dainty office-laptop scene", "overconfident delivery about a trivial preference", "luxury attitude applied to a mundane workflow". If there is no real mismatch, write "none".
|
|
1639
|
+
- "humor": if it's funny, state what the JOKE actually is and why it works, concretely enough that a remixer can rebuild the same joke with new subject matter; write "none" if the format is not comedic. Focus on the underlying bit, not surface choreography.
|
|
1640
|
+
- "audio_visual_tension": describe how the SOUND and VISUALS interact emotionally: whether the soundtrack / vocal cadence reinforces the visuals, undercuts them, or creates a deliberate mismatch. Name the exact pairing if that's the joke or swagger engine; write "none" only if audio contributes nothing.
|
|
1602
1641
|
- "delivery": the performance cues to preserve so the vibe survives — speech intonation / cadence (from the AUDIO TRANSCRIPT), the pause held before the punchline, a facial beat or reaction, and especially the SOUND: the music / trending-audio bed, where the beat drop or lyric hit lands relative to the cut, a music swell, an sfx sting, the deadpan-then-cut. This is what keeps intonation, comedic timing, and the audio-driven vibe from being lost in a remix.
|
|
1603
1642
|
- "preserve": 2-4 emotional beats a remix MUST keep to reproduce the same feeling (e.g. "keep the beat drop landing exactly on the reveal cut", "hold the silent beat before the reveal", "keep the flat unbothered delivery", "the payoff must undercut the buildup, not confirm it").
|
|
1604
|
-
Use BOTH the frames and the AUDIO TRANSCRIPT block above. Never leave emotional_punch empty — infer the feeling from the frames, captions, and pacing even when there is no audio, and when the audio is music-only, treat the sound itself as the emotional engine.
|
|
1643
|
+
Use BOTH the frames and the AUDIO TRANSCRIPT block above. Never leave emotional_punch empty — infer the feeling from the frames, captions, and pacing even when there is no audio, and when the audio is music-only, treat the sound itself as the emotional engine. Do NOT over-index on a single gesture or hand movement if the real emotional engine is the character attitude, the soundtrack choice, the setting mismatch, or the audio-vs-visual contrast.
|
|
1644
|
+
- "static_vs_pivot": this is the load-bearing reuse guidance for lazy prompts like "please recreate this video for my business". Make it operational, not vague.
|
|
1645
|
+
- "summary": one sentence on what can change versus what must stay.
|
|
1646
|
+
- "scene_replacement": decide whether most scenes should be replaced or only some, why, which scenes are load-bearing, and which scenes are the first candidates to swap.
|
|
1647
|
+
- "narration": decide whether the original spoken audio can stay as-is (for example because it is a generic or trending sound) or whether it must be customized because the words are source-specific. Set "customize_script" whenever the spoken words would stop making sense after a subject swap. If narration is timing-sensitive, set same_timing_required true.
|
|
1648
|
+
- "music": decide whether original music should stay, be recreated under a new narration, or is irrelevant. Include simple mix guidance like "keep low under VO" or "music is the main viral engine".
|
|
1649
|
+
- "captions": decide whether existing caption timing/animation can be reused, whether they must be re-timed to new narration, or whether captions are not important. Call out if the animation is tightly coupled to the spoken words.
|
|
1650
|
+
- "visual_rebuild": default strategy for replacement visuals. When the source mainly uses graphics, screenshots, product cards, or still-like scenes that do not need expensive motion video, prefer "ai_images_with_ken_burns" and explain that slow pan/zoom is enough.
|
|
1651
|
+
- "viral_dna": separate format elements into "must_keep", "flexible", and "do_not_change" so an agent can preserve the viral structure while swapping subject matter.
|
|
1605
1652
|
- "preserve": 3-5 concrete elements a remixer MUST keep to preserve the format (e.g., "Pinned comment stays on-screen the whole video", "Wedding dance payoff shot").
|
|
1606
1653
|
- "avoid": 3-5 things that would break the format if changed (e.g., "Do not remove the on-screen text overlay", "Do not slow the cuts below 1.5s").
|
|
1607
1654
|
- "promotions": 3-5 categories of product / service / offer this exact format could be reused to promote, WITHOUT changing the beat structure or hook mechanic. Each item is a concrete pitchable use-case, not a genre. Examples: "SaaS free-trial signup (before/after workflow)", "Fitness challenge (7-day transformation)", "Local restaurant grand-opening (line-out-the-door proof)", "Ebook / course launch (result-first payoff)", "Real-estate open-house (walk-through reveal)". Anchor each promotion to the same viral tactic captured in "hook" — do not invent unrelated verticals.
|
|
@@ -1781,10 +1828,36 @@ function resolveFontSize(input) {
|
|
|
1781
1828
|
return normalizeFontSize(input.fontSize, input.canvasHeight);
|
|
1782
1829
|
}
|
|
1783
1830
|
const DEFAULT_EMOTIONAL_PUNCH = {
|
|
1784
|
-
core_emotion: "",
|
|
1831
|
+
core_emotion: "",
|
|
1832
|
+
tone: "",
|
|
1833
|
+
arc: "",
|
|
1834
|
+
peak_moment: "",
|
|
1835
|
+
mechanism: "",
|
|
1836
|
+
contrast: "",
|
|
1837
|
+
humor: "",
|
|
1838
|
+
audio_visual_tension: "",
|
|
1839
|
+
delivery: "",
|
|
1840
|
+
preserve: []
|
|
1785
1841
|
};
|
|
1786
1842
|
export const DEFAULT_VIRAL_DNA = {
|
|
1787
|
-
trend_tagline: "",
|
|
1843
|
+
trend_tagline: "",
|
|
1844
|
+
hook: "",
|
|
1845
|
+
retention: "",
|
|
1846
|
+
payoff: "",
|
|
1847
|
+
emotional_punch: { ...DEFAULT_EMOTIONAL_PUNCH },
|
|
1848
|
+
static_vs_pivot: {
|
|
1849
|
+
summary: "",
|
|
1850
|
+
scene_replacement: { overall: "replace_some", reason: "", must_keep: [], should_replace: [], load_bearing_scenes: [] },
|
|
1851
|
+
narration: { overall: "optional", reason: "", same_timing_required: false, use_premium_tts_when_customizing: true },
|
|
1852
|
+
music: { overall: "optional", reason: "", volume_guidance: "" },
|
|
1853
|
+
captions: { overall: "optional", reason: "", animation_dependency: "" },
|
|
1854
|
+
visual_rebuild: { default_strategy: "mixed", reason: "" },
|
|
1855
|
+
viral_dna: { must_keep: [], flexible: [], do_not_change: [] }
|
|
1856
|
+
},
|
|
1857
|
+
preserve: [],
|
|
1858
|
+
avoid: [],
|
|
1859
|
+
promotions: [],
|
|
1860
|
+
keywords: []
|
|
1788
1861
|
};
|
|
1789
1862
|
function normalizeEmotionalPunch(value) {
|
|
1790
1863
|
if (!value || typeof value !== "object")
|
|
@@ -1800,11 +1873,89 @@ function normalizeEmotionalPunch(value) {
|
|
|
1800
1873
|
arc: asString(record.arc ?? record.emotional_arc),
|
|
1801
1874
|
peak_moment: asString(record.peak_moment ?? record.peakMoment),
|
|
1802
1875
|
mechanism: asString(record.mechanism),
|
|
1876
|
+
contrast: asString(record.contrast),
|
|
1803
1877
|
humor: asString(record.humor),
|
|
1878
|
+
audio_visual_tension: asString(record.audio_visual_tension ?? record.audioVisualTension),
|
|
1804
1879
|
delivery: asString(record.delivery),
|
|
1805
1880
|
preserve: asStringArray(record.preserve).slice(0, 4)
|
|
1806
1881
|
};
|
|
1807
1882
|
}
|
|
1883
|
+
function normalizeStaticVsPivot(value) {
|
|
1884
|
+
const fallback = DEFAULT_VIRAL_DNA.static_vs_pivot;
|
|
1885
|
+
if (!value || typeof value !== "object") {
|
|
1886
|
+
return {
|
|
1887
|
+
...fallback,
|
|
1888
|
+
scene_replacement: { ...fallback.scene_replacement },
|
|
1889
|
+
narration: { ...fallback.narration },
|
|
1890
|
+
music: { ...fallback.music },
|
|
1891
|
+
captions: { ...fallback.captions },
|
|
1892
|
+
visual_rebuild: { ...fallback.visual_rebuild },
|
|
1893
|
+
viral_dna: { ...fallback.viral_dna }
|
|
1894
|
+
};
|
|
1895
|
+
}
|
|
1896
|
+
const record = value;
|
|
1897
|
+
const asString = (v) => typeof v === "string" ? v.trim() : "";
|
|
1898
|
+
const asStringArray = (v) => Array.isArray(v)
|
|
1899
|
+
? v.map((item) => (typeof item === "string" ? item.trim() : "")).filter((item) => item.length > 0)
|
|
1900
|
+
: [];
|
|
1901
|
+
const sceneReplacement = record.scene_replacement && typeof record.scene_replacement === "object"
|
|
1902
|
+
? record.scene_replacement
|
|
1903
|
+
: {};
|
|
1904
|
+
const narration = record.narration && typeof record.narration === "object"
|
|
1905
|
+
? record.narration
|
|
1906
|
+
: {};
|
|
1907
|
+
const music = record.music && typeof record.music === "object"
|
|
1908
|
+
? record.music
|
|
1909
|
+
: {};
|
|
1910
|
+
const captions = record.captions && typeof record.captions === "object"
|
|
1911
|
+
? record.captions
|
|
1912
|
+
: {};
|
|
1913
|
+
const visualRebuild = record.visual_rebuild && typeof record.visual_rebuild === "object"
|
|
1914
|
+
? record.visual_rebuild
|
|
1915
|
+
: {};
|
|
1916
|
+
const viralDna = record.viral_dna && typeof record.viral_dna === "object"
|
|
1917
|
+
? record.viral_dna
|
|
1918
|
+
: {};
|
|
1919
|
+
const oneOf = (v, allowed, fallbackValue) => {
|
|
1920
|
+
const text = asString(v);
|
|
1921
|
+
return allowed.includes(text) ? text : fallbackValue;
|
|
1922
|
+
};
|
|
1923
|
+
return {
|
|
1924
|
+
summary: asString(record.summary),
|
|
1925
|
+
scene_replacement: {
|
|
1926
|
+
overall: oneOf(sceneReplacement.overall, ["replace_most", "replace_some", "keep_most", "keep_all"], "replace_some"),
|
|
1927
|
+
reason: asString(sceneReplacement.reason),
|
|
1928
|
+
must_keep: asStringArray(sceneReplacement.must_keep),
|
|
1929
|
+
should_replace: asStringArray(sceneReplacement.should_replace),
|
|
1930
|
+
load_bearing_scenes: asStringArray(sceneReplacement.load_bearing_scenes)
|
|
1931
|
+
},
|
|
1932
|
+
narration: {
|
|
1933
|
+
overall: oneOf(narration.overall, ["keep_original", "customize_script", "optional"], "optional"),
|
|
1934
|
+
reason: asString(narration.reason),
|
|
1935
|
+
same_timing_required: narration.same_timing_required === true,
|
|
1936
|
+
use_premium_tts_when_customizing: narration.use_premium_tts_when_customizing !== false
|
|
1937
|
+
},
|
|
1938
|
+
music: {
|
|
1939
|
+
overall: oneOf(music.overall, ["keep_original", "recreate_under_narration", "optional", "none"], "optional"),
|
|
1940
|
+
reason: asString(music.reason),
|
|
1941
|
+
volume_guidance: asString(music.volume_guidance)
|
|
1942
|
+
},
|
|
1943
|
+
captions: {
|
|
1944
|
+
overall: oneOf(captions.overall, ["reuse_existing", "retime_to_new_narration", "optional", "none"], "optional"),
|
|
1945
|
+
reason: asString(captions.reason),
|
|
1946
|
+
animation_dependency: asString(captions.animation_dependency)
|
|
1947
|
+
},
|
|
1948
|
+
visual_rebuild: {
|
|
1949
|
+
default_strategy: oneOf(visualRebuild.default_strategy, ["ai_images_with_ken_burns", "real_footage", "mixed"], "mixed"),
|
|
1950
|
+
reason: asString(visualRebuild.reason)
|
|
1951
|
+
},
|
|
1952
|
+
viral_dna: {
|
|
1953
|
+
must_keep: asStringArray(viralDna.must_keep),
|
|
1954
|
+
flexible: asStringArray(viralDna.flexible),
|
|
1955
|
+
do_not_change: asStringArray(viralDna.do_not_change)
|
|
1956
|
+
}
|
|
1957
|
+
};
|
|
1958
|
+
}
|
|
1808
1959
|
function normalizeViralDna(value) {
|
|
1809
1960
|
if (!value || typeof value !== "object")
|
|
1810
1961
|
return DEFAULT_VIRAL_DNA;
|
|
@@ -1819,6 +1970,7 @@ function normalizeViralDna(value) {
|
|
|
1819
1970
|
retention: asString(record.retention),
|
|
1820
1971
|
payoff: asString(record.payoff),
|
|
1821
1972
|
emotional_punch: normalizeEmotionalPunch(record.emotional_punch ?? record.emotionalPunch),
|
|
1973
|
+
static_vs_pivot: normalizeStaticVsPivot(record.static_vs_pivot ?? record.staticVsPivot),
|
|
1822
1974
|
preserve: asStringArray(record.preserve),
|
|
1823
1975
|
avoid: asStringArray(record.avoid),
|
|
1824
1976
|
promotions: asStringArray(record.promotions),
|
|
@@ -1865,12 +2017,26 @@ function buildEditorHarnessPrompt(input) {
|
|
|
1865
2017
|
const roleVocab = SCENE_ROLE_VOCAB.join(" | ");
|
|
1866
2018
|
const formatVocab = CONTENT_FORMATS.join(" | ");
|
|
1867
2019
|
const audioDigest = buildAudioDigestForPrompt(input.transcript ?? null);
|
|
2020
|
+
const emotional = input.viralDna?.emotional_punch;
|
|
2021
|
+
const emotionalSnapshot = emotional && [
|
|
2022
|
+
emotional.core_emotion,
|
|
2023
|
+
emotional.tone,
|
|
2024
|
+
emotional.arc,
|
|
2025
|
+
emotional.peak_moment,
|
|
2026
|
+
emotional.mechanism,
|
|
2027
|
+
emotional.contrast,
|
|
2028
|
+
emotional.humor,
|
|
2029
|
+
emotional.audio_visual_tension,
|
|
2030
|
+
emotional.delivery
|
|
2031
|
+
].some((value) => value && value.trim().length > 0)
|
|
2032
|
+
? `\nVIRAL DNA FEELING SNAPSHOT (operationalize this in the harness; do NOT drift from it):\n- core_emotion: ${emotional.core_emotion || "—"}\n- tone: ${emotional.tone || "—"}\n- arc: ${emotional.arc || "—"}\n- peak_moment: ${emotional.peak_moment || "—"}\n- mechanism: ${emotional.mechanism || "—"}\n- contrast: ${emotional.contrast || "—"}\n- humor: ${emotional.humor || "—"}\n- audio_visual_tension: ${emotional.audio_visual_tension || "—"}\n- delivery: ${emotional.delivery || "—"}\n- preserve: ${(emotional.preserve || []).join("; ") || "—"}\n`
|
|
2033
|
+
: "";
|
|
1868
2034
|
return `You are Vidfarm's EDITING DIRECTOR. You are studying a short-form video to write an EDITOR HARNESS — a compact, technical brief that lets another AI editor recreate this video's STYLE (not its exact content) in one clean pass on a timeline editor (scenes, captions, audio, transitions, keyframes).
|
|
1869
2035
|
|
|
1870
2036
|
This is DIFFERENT from "viral DNA" (why the video works / what to preserve). The harness is HOW to edit: pace, typography, b-roll usage, transitions, audio bed, and the role each scene plays. Be concrete and directive — every field should be something an editor can act on, not vibes.
|
|
1871
2037
|
|
|
1872
2038
|
Video duration: ${input.durationSeconds.toFixed(2)} seconds.
|
|
1873
|
-
${frameListing}${cutListing}${audioDigest}${promptHint}
|
|
2039
|
+
${frameListing}${cutListing}${audioDigest}${emotionalSnapshot}${promptHint}
|
|
1874
2040
|
Return strictly valid JSON matching this schema:
|
|
1875
2041
|
{
|
|
1876
2042
|
"format": string, // one of: ${formatVocab}
|
|
@@ -1933,7 +2099,7 @@ RULES:
|
|
|
1933
2099
|
- Ground pacing numbers on the detected cuts + duration when cuts are supplied; otherwise estimate from the frames. Round avg_scene_seconds and cuts_per_10s to at most 2 decimals.
|
|
1934
2100
|
- "scenes": 3-8 entries covering the whole runtime in order, each tied to a real moment you see. Reserve importance "critical" for the 1-3 beats that actually carry the style/retention (usually the hook + payoff).
|
|
1935
2101
|
- Prefer the caption_style / transition / shot_kind vocabulary above so the editor can map straight to presets; a free-form label is fine when nothing fits.
|
|
1936
|
-
- "emotional": this is HOW to edit so the FEELING survives — the vibe, the joke, the intonation, and the SOUND are the first things a remix flattens, so make these directive. Use the AUDIO TRANSCRIPT block: on TikTok/Reels the trending sound / music bed is often the biggest carrier of the feeling. "target_feeling" = the emotion the recreated edit must produce; "tone" = the register to hold; "comedic_timing" = the exact editing move that sells the joke (the beat held before a cut, the deadpan-then-punch, the sfx sting or beat-drop on the button word) or "none"; "intonation" = the vocal delivery/cadence to preserve on any VO or on-cam speech (or "none" if silent) — flag it if the speech must be re-voiced in the SAME cadence; "vibe_anchors" = 2-4 concrete edit choices that carry the emotion, and include the audio ones (e.g. "keep the trending sound and land the beat drop on the reveal cut", "swell the music on the payoff", "hold a full beat of silence before the reveal cut", "cut hard on the punch word"). Infer the feeling from frames + pacing even without audio, and when the audio is music-only treat the sound as the emotional engine; never leave it empty.
|
|
2102
|
+
- "emotional": this is HOW to edit so the FEELING survives — the vibe, the joke, the intonation, and the SOUND are the first things a remix flattens, so make these directive. Use the AUDIO TRANSCRIPT block: on TikTok/Reels the trending sound / music bed is often the biggest carrier of the feeling. "target_feeling" = the emotion the recreated edit must produce; "tone" = the register to hold; "comedic_timing" = the exact editing move that sells the joke (the beat held before a cut, the deadpan-then-punch, the sfx sting or beat-drop on the button word) or "none"; "intonation" = the vocal delivery/cadence to preserve on any VO or on-cam speech (or "none" if silent) — flag it if the speech must be re-voiced in the SAME cadence; "vibe_anchors" = 2-4 concrete edit choices that carry the emotion, and include the audio ones (e.g. "keep the trending sound and land the beat drop on the reveal cut", "swell the music on the payoff", "hold a full beat of silence before the reveal cut", "cut hard on the punch word"). When the viral-DNA snapshot above names a contrast or audio_visual_tension, convert that into actionable edit moves instead of drifting to superficial gestures. Infer the feeling from frames + pacing even without audio, and when the audio is music-only treat the sound as the emotional engine; never leave it empty.
|
|
1937
2103
|
- Keep every string tight and directive. Never invent audio you cannot infer — say "none"/"unknown" instead.
|
|
1938
2104
|
- Output ONLY the JSON object.`;
|
|
1939
2105
|
}
|
|
@@ -2030,7 +2196,8 @@ async function runEditorHarnessPass(input) {
|
|
|
2030
2196
|
frameTimestamps: input.frames.map((f) => f.timestamp),
|
|
2031
2197
|
sceneCuts: input.sceneCuts,
|
|
2032
2198
|
userPrompt: input.userPrompt ?? null,
|
|
2033
|
-
transcript: input.transcript ?? null
|
|
2199
|
+
transcript: input.transcript ?? null,
|
|
2200
|
+
viralDna: input.viralDna ?? null
|
|
2034
2201
|
});
|
|
2035
2202
|
const attempt = await callVisionWithFallback({ prompt, frames: input.frames, keys: input.keys });
|
|
2036
2203
|
return normalizeEditorHarness(attempt.result);
|
|
@@ -2437,13 +2604,12 @@ export async function smartDecomposeVideo(input) {
|
|
|
2437
2604
|
sceneCuts,
|
|
2438
2605
|
transcript
|
|
2439
2606
|
});
|
|
2440
|
-
// Phase 2 — vision, OCR, product-placement
|
|
2441
|
-
//
|
|
2442
|
-
//
|
|
2443
|
-
//
|
|
2444
|
-
//
|
|
2445
|
-
|
|
2446
|
-
const [attempt, ocrFrames, placementSurfaces, editorHarness] = await Promise.all([
|
|
2607
|
+
// Phase 2 — vision, OCR, and product-placement run concurrently, all now
|
|
2608
|
+
// transcript-aware. OCR and placement failures are non-fatal. The
|
|
2609
|
+
// placement call is DELIBERATELY separate from the caption/scene vision
|
|
2610
|
+
// call so it can never dilute timeline-layer quality — it reuses the same
|
|
2611
|
+
// sparse frames (no extra ffmpeg) plus the transcript from phase 1.
|
|
2612
|
+
const [attempt, ocrFrames, placementSurfaces] = await Promise.all([
|
|
2447
2613
|
callVisionWithFallback({
|
|
2448
2614
|
prompt,
|
|
2449
2615
|
frames,
|
|
@@ -2461,19 +2627,6 @@ export async function smartDecomposeVideo(input) {
|
|
|
2461
2627
|
}).catch((err) => {
|
|
2462
2628
|
console.error("smart decompose placement pass failed", err instanceof Error ? err.message : err);
|
|
2463
2629
|
return [];
|
|
2464
|
-
}),
|
|
2465
|
-
// Editor harness — technical editing direction. Isolated call in the same
|
|
2466
|
-
// burst (reuses these frames + the detected cuts + the transcript); non-fatal.
|
|
2467
|
-
runEditorHarnessPass({
|
|
2468
|
-
frames,
|
|
2469
|
-
keys: input.keys,
|
|
2470
|
-
durationSeconds: meta.durationSeconds,
|
|
2471
|
-
sceneCuts,
|
|
2472
|
-
userPrompt: input.userPrompt ?? null,
|
|
2473
|
-
transcript
|
|
2474
|
-
}).catch((err) => {
|
|
2475
|
-
console.error("smart decompose editor-harness pass failed", err instanceof Error ? err.message : err);
|
|
2476
|
-
return DEFAULT_EDITOR_HARNESS;
|
|
2477
2630
|
})
|
|
2478
2631
|
]);
|
|
2479
2632
|
devLog("hyperframes.smart_decompose.ocr_completed", {
|
|
@@ -2559,6 +2712,21 @@ export async function smartDecomposeVideo(input) {
|
|
|
2559
2712
|
if (input.trendTaglineHint && input.trendTaglineHint.trim().length > 0) {
|
|
2560
2713
|
viralDna.trend_tagline = input.trendTaglineHint.trim();
|
|
2561
2714
|
}
|
|
2715
|
+
// Editor harness — technical editing direction. Run AFTER viral DNA is
|
|
2716
|
+
// normalized so the harness can operationalize the exact emotional punch
|
|
2717
|
+
// instead of guessing independently and drifting from the decompose result.
|
|
2718
|
+
const editorHarness = await runEditorHarnessPass({
|
|
2719
|
+
frames,
|
|
2720
|
+
keys: input.keys,
|
|
2721
|
+
durationSeconds: meta.durationSeconds,
|
|
2722
|
+
sceneCuts,
|
|
2723
|
+
userPrompt: input.userPrompt ?? null,
|
|
2724
|
+
transcript,
|
|
2725
|
+
viralDna
|
|
2726
|
+
}).catch((err) => {
|
|
2727
|
+
console.error("smart decompose editor-harness pass failed", err instanceof Error ? err.message : err);
|
|
2728
|
+
return DEFAULT_EDITOR_HARNESS;
|
|
2729
|
+
});
|
|
2562
2730
|
// NOTE: the per-scene clip-annotation + content-format pass is deliberately
|
|
2563
2731
|
// NOT run here — it is a separate vision call that would risk the ~60s origin
|
|
2564
2732
|
// timeout on longer videos. /auto-decompose seeds scene-annotations.json
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mevdragon/vidfarm-devcli",
|
|
3
|
-
"version": "0.20.
|
|
3
|
+
"version": "0.20.6",
|
|
4
4
|
"description": "Local bridge for the Vidfarm Trackpad Editor. `vidfarm serve <template_id>` boots the FULL editor on localhost (disk-backed records/storage, free in-process render); edit composition.html on disk (Claude Code, Codex, etc.) and the browser live-morphs it.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
"scripts": {
|
|
33
33
|
"dev": "tsx --import ./src/instrument.ts watch src/index.ts",
|
|
34
34
|
"dev:frontend": "node scripts/build-homepage-client.mjs --watch",
|
|
35
|
-
"dev:cli": "tsx src/cli.ts
|
|
35
|
+
"dev:cli": "tsx src/cli.ts",
|
|
36
36
|
"build:frontend": "node scripts/build-homepage-client.mjs",
|
|
37
37
|
"build:hyperframes-editor": "cd demo && node scripts/build.mjs",
|
|
38
38
|
"operator:template-artifact": "node scripts/platform-operator-template-artifact.mjs",
|