@officexapp/vidfarm-devcli 0.21.43 → 0.21.45
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/.agents/skills/editor-capabilities/SKILL.md +2 -0
- package/.agents/skills/vidfarm/SKILL.md +37 -12
- package/.agents/skills/vidfarm/recipes/onboard-a-new-director.md +1 -1
- package/.agents/skills/vidfarm/references/assets-and-sourcing.md +97 -1
- package/.agents/skills/vidfarm/references/automation-and-local-dev.md +11 -5
- package/.agents/skills/vidfarm/references/content-ideas.md +232 -10
- package/.agents/skills/vidfarm/references/core-workflows.md +11 -1
- package/.agents/skills/vidfarm/references/editor-workflows.md +17 -0
- package/.agents/skills/vidfarm/references/onboarding.md +1 -1
- package/.agents/skills/vidfarm/references/primitives.md +51 -0
- package/.agents/skills/vidfarm-media/SKILL.md +2 -0
- package/SKILL.director.md +458 -31
- package/SKILL.md +117 -116
- package/crowdsourcing.md +44 -1
- package/dist/src/cli.js +544 -31
- package/dist/src/devcli/consult.js +14 -0
- package/dist/src/devcli/skill-docs.js +61 -7
- package/dist/src/services/clip-curation/index.js +1 -1
- package/dist/src/services/clip-curation/media-select.js +146 -3
- package/experimental/google-news-to-video.md +235 -0
- package/package.json +4 -1
- package/public/assets/file-directory-app.js +28 -28
- package/public/assets/homepage-client-app.js +15 -15
|
@@ -63,6 +63,18 @@ const STEP_ORDER = ["coldstart", "awareness", "angles", "hooks"];
|
|
|
63
63
|
* answered. It lives in the working folder next to the other artifacts.
|
|
64
64
|
*/
|
|
65
65
|
export const CONSULT_CONTEXT_FILE = "CONTEXT.md";
|
|
66
|
+
/**
|
|
67
|
+
* CONTENT IDEAS COME BEFORE THE INTERVIEW.
|
|
68
|
+
*
|
|
69
|
+
* An interview asks the director to work before they have seen anything, which
|
|
70
|
+
* is the worst trade a first session can offer. `vidfarm ideas --topic "<their
|
|
71
|
+
* offer in a line>"` inverts it: one sentence in, 20+ titled video ideas out,
|
|
72
|
+
* offline and free. The director gets a win first, and the ideas they react to
|
|
73
|
+
* ("this one, not that one") are better interview answers than the ones they
|
|
74
|
+
* would have typed cold. So every interview brief opens by offering the ideas
|
|
75
|
+
* pass, and the interview becomes the thing they opt INTO once they want depth.
|
|
76
|
+
*/
|
|
77
|
+
const IDEAS_FIRST = 'START WITH CONTENT IDEAS, NOT THIS INTERVIEW, unless the director already ran them or explicitly asked for a consultation. Get their offer in ONE line, run `vidfarm ideas --topic "<that line>"`, sharpen the frames into 20+ real titles and save `content-ideas.md`. It is offline, free, and gives them something to react to. Then say the interview is available when they want the strategy behind the ideas — and run it now only if they say yes.';
|
|
66
78
|
/**
|
|
67
79
|
* The SHORT interview. The full cold-start prompt asks a model to invent 12-30
|
|
68
80
|
* questions; that is the right depth for a director who wants a real strategy
|
|
@@ -244,6 +256,7 @@ export function buildConsultBrief(input) {
|
|
|
244
256
|
prompt: buildShortInterviewPrompt(),
|
|
245
257
|
steps: [
|
|
246
258
|
contextRead,
|
|
259
|
+
IDEAS_FIRST,
|
|
247
260
|
`Ask the human these ${SHORT_INTERVIEW.length} questions in two batches of three. Ask them as written. Do not add questions, do not invent answers, and do not skip ahead to advice.`,
|
|
248
261
|
`Keep their phrasing for the pain and the desire answers word for word: the angles and hooks steps quote them back, and a paraphrase is what makes an ad sound generic.`,
|
|
249
262
|
"TELL THEM UP FRONT that every question is optional: they can skip any one, or say stop and end the interview on the spot. Then work with whatever they gave — do not push, do not re-ask, and do not make finishing the set a condition of getting a video.",
|
|
@@ -264,6 +277,7 @@ export function buildConsultBrief(input) {
|
|
|
264
277
|
prompt: buildColdstartPrompt(input.userMessage?.trim() || "The director asked for a consultation and has given no offer context yet.", count),
|
|
265
278
|
steps: [
|
|
266
279
|
...common,
|
|
280
|
+
IDEAS_FIRST,
|
|
267
281
|
`This one is an INTERVIEW, not a report. Answer the prompt to get the question set, then ASK THE HUMAN those questions conversationally, a few at a time — never dump all ${count} at once, and never invent their answers.`,
|
|
268
282
|
`Assemble their real words into \`${artifact}\`: keep their phrasing for pain and desire, because the later angle and hook steps quote it back.`,
|
|
269
283
|
"Stop and wait for the human between batches. If they go quiet on a question, mark it UNKNOWN in the file rather than guessing — an invented customer psyche poisons every later step.",
|
|
@@ -108,7 +108,15 @@ export function readPackDoc(ref, name = DEFAULT_PACK) {
|
|
|
108
108
|
}
|
|
109
109
|
export const PACK_TOPICS = [
|
|
110
110
|
{ topic: "content-ideas", aliases: ["ideas", "idea", "angles", "what-to-post", "content"], doc: "references/content-ideas.md",
|
|
111
|
-
blurb: "
|
|
111
|
+
blurb: "50 frames x 5 awareness stages x 44 problem angles — answer \"what should I post?\" for a whole month" },
|
|
112
|
+
// The other two idea axes get their own spoken names. A director asks "what
|
|
113
|
+
// is awareness again?" far more often than they ask for the content-ideas
|
|
114
|
+
// reference by file name, and sending them the whole 290-line doc for a
|
|
115
|
+
// 40-line answer is how a topic index fails.
|
|
116
|
+
{ topic: "awareness", aliases: ["awareness-stages", "ladder", "stages", "schwartz"], doc: "references/content-ideas.md", heading: "The awareness ladder",
|
|
117
|
+
blurb: "The 5-stage ladder — what the viewer knows, what the video must do, and what it may ask for" },
|
|
118
|
+
{ topic: "problem-angles", aliases: ["angle", "lenses", "problem-angle"], doc: "references/content-ideas.md", heading: "The problem angles",
|
|
119
|
+
blurb: "44 angles on the problem — hold the frame, change the angle when a topic is \"already covered\"" },
|
|
112
120
|
{ topic: "meme-recaption", aliases: ["meme", "recaption", "meme-caption"], doc: "references/editor-workflows.md", heading: "Writing a meme recaption",
|
|
113
121
|
blurb: "Recaption a meme at a pain or a win the niche knows — the cold-viewer test" },
|
|
114
122
|
{ topic: "product-explainer", aliases: ["product-explainers"], doc: "harnesses/product-explainer.HARNESS.md",
|
|
@@ -188,10 +196,13 @@ export function readPackTopic(topic, name = DEFAULT_PACK) {
|
|
|
188
196
|
return { doc, contents, whole: true };
|
|
189
197
|
return { doc, heading: section.heading, contents: section.body, whole: false };
|
|
190
198
|
}
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
199
|
+
/**
|
|
200
|
+
* Shared shape for both bullet banks: a bold family label on its own line,
|
|
201
|
+
* then `- item` (frames) or `- item — note` (angles) bullets under it.
|
|
202
|
+
*/
|
|
203
|
+
function parseBulletBank(contents, heading, splitNote) {
|
|
204
|
+
const section = extractSection(contents, heading);
|
|
205
|
+
const lines = (section?.body ?? "").split("\n");
|
|
195
206
|
const frames = [];
|
|
196
207
|
const families = [];
|
|
197
208
|
let family = "";
|
|
@@ -204,11 +215,54 @@ export function loadIdeaBank(name = DEFAULT_PACK) {
|
|
|
204
215
|
continue;
|
|
205
216
|
}
|
|
206
217
|
const bullet = line.match(/^-\s+(.+?)\s*$/);
|
|
207
|
-
if (bullet
|
|
208
|
-
|
|
218
|
+
if (!bullet || !family)
|
|
219
|
+
continue;
|
|
220
|
+
const text = bullet[1].trim();
|
|
221
|
+
if (!splitNote) {
|
|
222
|
+
frames.push({ frame: text, family });
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
const at = text.indexOf(" — ");
|
|
226
|
+
frames.push(at > 0
|
|
227
|
+
? { frame: text.slice(0, at).trim(), family, note: text.slice(at + 3).trim() }
|
|
228
|
+
: { frame: text, family });
|
|
209
229
|
}
|
|
210
230
|
return { frames, families };
|
|
211
231
|
}
|
|
232
|
+
export function loadIdeaBank(name = DEFAULT_PACK) {
|
|
233
|
+
const { contents } = readPackDoc("references/content-ideas.md", name);
|
|
234
|
+
const parsed = parseBulletBank(contents, "The 50 frames", false);
|
|
235
|
+
// Old packs predate the section slice; fall back to the whole file rather
|
|
236
|
+
// than handing the CLI an empty bank.
|
|
237
|
+
return parsed.frames.length ? parsed : parseBulletBank(`## all\n${contents}`, "all", false);
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* The problem-angle bank — the SECOND axis of an idea. A frame says what the
|
|
241
|
+
* video is the story of; an angle says which side of the problem it approaches
|
|
242
|
+
* from. Holding the frame and changing the angle is the cheapest way to answer
|
|
243
|
+
* "I already covered that topic", so this has to be as reachable as the frames.
|
|
244
|
+
*/
|
|
245
|
+
export function loadAngleBank(name = DEFAULT_PACK) {
|
|
246
|
+
const { contents } = readPackDoc("references/content-ideas.md", name);
|
|
247
|
+
const { frames, families } = parseBulletBank(contents, "The problem angles", true);
|
|
248
|
+
return { angles: frames, families };
|
|
249
|
+
}
|
|
250
|
+
export function loadAwarenessLadder(name = DEFAULT_PACK) {
|
|
251
|
+
const { contents } = readPackDoc("references/content-ideas.md", name);
|
|
252
|
+
const section = extractSection(contents, "The awareness ladder");
|
|
253
|
+
const stages = [];
|
|
254
|
+
for (const line of (section?.body ?? "").split("\n")) {
|
|
255
|
+
const head = line.match(/^\*\*Stage\s+(\d+)\s*·\s*(.+?)\*\*\s*[—-]\s*(.+?)\s*$/);
|
|
256
|
+
if (head) {
|
|
257
|
+
stages.push({ index: Number(head[1]), stage: head[2].trim(), summary: head[3].trim(), fields: [] });
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
const field = line.match(/^-\s+\*\*(.+?):\*\*\s*(.+?)\s*$/);
|
|
261
|
+
if (field && stages.length)
|
|
262
|
+
stages[stages.length - 1].fields.push({ label: field[1].trim(), value: field[2].trim() });
|
|
263
|
+
}
|
|
264
|
+
return stages;
|
|
265
|
+
}
|
|
212
266
|
/**
|
|
213
267
|
* Grep the pack. This is the affordance that makes a local copy genuinely
|
|
214
268
|
* better than the network one: "where does it say anything about greenscreen"
|
|
@@ -12,7 +12,7 @@ export { cosineSimilarity, matchesCriteria, rankHits, scoreClip, searchClips, st
|
|
|
12
12
|
export { estimateScanCostFromDuration, estimateScanCostFromScenes, formatCostEstimate } from "./cost.js";
|
|
13
13
|
export { processSceneToClip, scanVideo } from "./scan.js";
|
|
14
14
|
export { refineScenesWithGuidance } from "./refine.js";
|
|
15
|
-
export { estimateMediaHeight, pickBestVideoMedia, rankPlayableVideoMedias, fetchFirstDownloadableMedia } from "./media-select.js";
|
|
15
|
+
export { estimateMediaHeight, pickBestVideoMedia, rankPlayableVideoMedias, fetchFirstDownloadableMedia, fetchSocialDownloadLookup, normalizeSocialDownloadLookup, normalizeSocialDownloadMedia, DEFAULT_SOCIAL_DOWNLOAD_URL, DEFAULT_SOCIAL_DOWNLOAD_HOST } from "./media-select.js";
|
|
16
16
|
export { deriveMediaNameFromUrl, resolveRawSourceName, slugifyFolderName } from "./source-naming.js";
|
|
17
17
|
// Re-export ClipPreset-related helpers already covered by ./presets and ./types
|
|
18
18
|
// via the wildcard exports above; nothing extra needed here.
|
|
@@ -4,6 +4,138 @@
|
|
|
4
4
|
// by whatever source we ingest, so we must pick the HIGHEST-resolution playable
|
|
5
5
|
// MP4 — not just the first one. Shared by the clip-scan Lambda (infra/lambda)
|
|
6
6
|
// and the in-process serve/import path (src/app.ts) so both stay in lock-step.
|
|
7
|
+
// ── Provider: social-download-all-in-one (RapidAPI) ──────────────────────────
|
|
8
|
+
// The single social resolver behind every URL ingest. It takes a JSON body
|
|
9
|
+
// (`{url}`) and answers 200 for BOTH success and failure — a failed lookup is
|
|
10
|
+
// `{"error":true,"status":404,"message":"Not found data"}` — so the HTTP status
|
|
11
|
+
// alone never tells you whether there is media. Its per-platform quirks are
|
|
12
|
+
// normalized here, once, rather than in each of the five call sites:
|
|
13
|
+
//
|
|
14
|
+
// youtube `medias[].ext`, quality "mp4 (1080p)", every rendition above 360p
|
|
15
|
+
// is a DASH VIDEO-ONLY stream, and 2160p/1440p mp4 are AV1
|
|
16
|
+
// tiktok byte size arrives as `data_size`; qualities are
|
|
17
|
+
// hd_no_watermark / no_watermark / watermark (hd is usually HEVC);
|
|
18
|
+
// `duration` is in MILLISECONDS
|
|
19
|
+
// instagram quality is "WxHp" ("720x1280p"); carries `is_audio`
|
|
20
|
+
// x one entry plus an alternate-bitrate `formats[]` (includes an m3u8)
|
|
21
|
+
//
|
|
22
|
+
export const DEFAULT_SOCIAL_DOWNLOAD_URL = "https://social-download-all-in-one.p.rapidapi.com/v1/social/autolink";
|
|
23
|
+
export const DEFAULT_SOCIAL_DOWNLOAD_HOST = "social-download-all-in-one.p.rapidapi.com";
|
|
24
|
+
/** TikTok posts cap at 10 minutes, so any larger `duration` is milliseconds. */
|
|
25
|
+
const TIKTOK_MAX_DURATION_SEC = 600;
|
|
26
|
+
/** Audio codecs that can appear in an MP4/WebM `codecs="…"` parameter. */
|
|
27
|
+
const AUDIO_CODEC_RE = /(^|[,\s])(mp4a|opus|vorbis|ac-3|ec-3|flac|alac|mp3)/i;
|
|
28
|
+
/**
|
|
29
|
+
* Does this rendition carry audio? See `DownloadMediaCandidate.hasAudioTrack`
|
|
30
|
+
* for why `undefined` (unknown) must NOT be read as "no".
|
|
31
|
+
*/
|
|
32
|
+
function detectMuxedAudio(raw) {
|
|
33
|
+
if (raw.type === "audio")
|
|
34
|
+
return true;
|
|
35
|
+
if (raw.is_audio === true)
|
|
36
|
+
return true;
|
|
37
|
+
const codecs = String(raw.mimeType ?? "").match(/codecs="([^"]+)"/i)?.[1];
|
|
38
|
+
// A declared codec list is authoritative: YouTube's muxed itag-18 rendition
|
|
39
|
+
// lists `avc1…, mp4a…`, its DASH renditions list the video codec alone.
|
|
40
|
+
if (codecs)
|
|
41
|
+
return AUDIO_CODEC_RE.test(codecs);
|
|
42
|
+
if (raw.audioQuality)
|
|
43
|
+
return true;
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
function toFiniteNumber(value) {
|
|
47
|
+
const parsed = typeof value === "number" ? value : Number(value);
|
|
48
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : undefined;
|
|
49
|
+
}
|
|
50
|
+
/** Map one provider media entry onto the canonical candidate shape. */
|
|
51
|
+
export function normalizeSocialDownloadMedia(entry) {
|
|
52
|
+
const raw = (entry && typeof entry === "object" ? entry : {});
|
|
53
|
+
const type = typeof raw.type === "string" ? raw.type : undefined;
|
|
54
|
+
const hasAudioTrack = detectMuxedAudio(raw);
|
|
55
|
+
return {
|
|
56
|
+
url: typeof raw.url === "string" ? raw.url : undefined,
|
|
57
|
+
quality: typeof raw.quality === "string" ? raw.quality : undefined,
|
|
58
|
+
label: typeof raw.label === "string" ? raw.label : undefined,
|
|
59
|
+
// YouTube uses `ext`; every other platform uses `extension`.
|
|
60
|
+
extension: typeof raw.extension === "string" ? raw.extension : typeof raw.ext === "string" ? raw.ext : undefined,
|
|
61
|
+
type,
|
|
62
|
+
width: toFiniteNumber(raw.width),
|
|
63
|
+
height: toFiniteNumber(raw.height),
|
|
64
|
+
size: toFiniteNumber(raw.size) ?? toFiniteNumber(raw.data_size),
|
|
65
|
+
mimeType: typeof raw.mimeType === "string" ? raw.mimeType : undefined,
|
|
66
|
+
hasAudioTrack,
|
|
67
|
+
// The provider no longer sends the old availability flags, so derive them
|
|
68
|
+
// from the declared `type` — downstream selectors still read them.
|
|
69
|
+
videoAvailable: type ? type === "video" : undefined,
|
|
70
|
+
audioAvailable: type === "audio" ? true : type === "video" ? hasAudioTrack : undefined
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
function normalizeLookupDuration(rawDuration, source) {
|
|
74
|
+
const parsed = typeof rawDuration === "number" ? rawDuration : Number(rawDuration);
|
|
75
|
+
if (!Number.isFinite(parsed) || parsed <= 0)
|
|
76
|
+
return null;
|
|
77
|
+
if (source === "tiktok" && parsed > TIKTOK_MAX_DURATION_SEC)
|
|
78
|
+
return Number((parsed / 1000).toFixed(3));
|
|
79
|
+
return parsed;
|
|
80
|
+
}
|
|
81
|
+
function trimmedOrNull(value) {
|
|
82
|
+
return typeof value === "string" && value.trim() ? value.trim() : null;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Normalize a raw provider response, throwing its own `{error, status, message}`
|
|
86
|
+
* failure as an Error. Split from the fetch so tests (and any caller holding an
|
|
87
|
+
* already-parsed body) can use it directly.
|
|
88
|
+
*/
|
|
89
|
+
export function normalizeSocialDownloadLookup(body) {
|
|
90
|
+
const raw = (body && typeof body === "object" ? body : {});
|
|
91
|
+
if (raw.error === true || (raw.error && typeof raw.error === "string")) {
|
|
92
|
+
const message = trimmedOrNull(raw.message) ?? (typeof raw.error === "string" ? raw.error : null) ?? "lookup failed";
|
|
93
|
+
const status = raw.status ? ` (status ${String(raw.status)})` : "";
|
|
94
|
+
throw new Error(`Video download lookup failed: ${message}${status}`);
|
|
95
|
+
}
|
|
96
|
+
const source = trimmedOrNull(raw.source);
|
|
97
|
+
return {
|
|
98
|
+
url: trimmedOrNull(raw.url),
|
|
99
|
+
source,
|
|
100
|
+
title: trimmedOrNull(raw.title),
|
|
101
|
+
author: trimmedOrNull(raw.author),
|
|
102
|
+
thumbnail: trimmedOrNull(raw.thumbnail),
|
|
103
|
+
duration: normalizeLookupDuration(raw.duration, source),
|
|
104
|
+
medias: Array.isArray(raw.medias) ? raw.medias.map(normalizeSocialDownloadMedia) : []
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* One lookup call, normalized. Every URL-ingest path in the codebase (the
|
|
109
|
+
* video_download primitive, the clip-scan Lambda, the clipper preview, the
|
|
110
|
+
* in-process import, the seed scripts) goes through here so the provider's
|
|
111
|
+
* request shape and its per-platform quirks live in exactly one place.
|
|
112
|
+
*/
|
|
113
|
+
export async function fetchSocialDownloadLookup(input) {
|
|
114
|
+
const endpoint = (input.endpoint || "").trim() || DEFAULT_SOCIAL_DOWNLOAD_URL;
|
|
115
|
+
const host = (input.host || "").trim() || DEFAULT_SOCIAL_DOWNLOAD_HOST;
|
|
116
|
+
const response = await fetch(endpoint, {
|
|
117
|
+
method: "POST",
|
|
118
|
+
headers: {
|
|
119
|
+
"x-rapidapi-key": input.apiKey,
|
|
120
|
+
"x-rapidapi-host": host,
|
|
121
|
+
"content-type": "application/json"
|
|
122
|
+
},
|
|
123
|
+
body: JSON.stringify({ url: input.sourceUrl })
|
|
124
|
+
});
|
|
125
|
+
if (input.onApiCall) {
|
|
126
|
+
try {
|
|
127
|
+
await input.onApiCall({ sourceUrl: input.sourceUrl, host, status: response.status });
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
// metering is best-effort; never fail the download over it
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
if (!response.ok) {
|
|
134
|
+
const details = await response.text().catch(() => "");
|
|
135
|
+
throw new Error(`Video download lookup failed with HTTP ${response.status}${details ? `: ${details.slice(0, 300)}` : ""}`);
|
|
136
|
+
}
|
|
137
|
+
return normalizeSocialDownloadLookup(await response.json());
|
|
138
|
+
}
|
|
7
139
|
/** True when a candidate is a directly-downloadable MP4 (not HLS/DASH/audio). */
|
|
8
140
|
function isPlayableMp4(m) {
|
|
9
141
|
if (!m.url)
|
|
@@ -88,9 +220,20 @@ export function rankPlayableVideoMedias(medias) {
|
|
|
88
220
|
.map((m, index) => ({ m, index, h: estimateMediaHeight(m) }))
|
|
89
221
|
.sort((a, b) => b.h - a.h || a.index - b.index)
|
|
90
222
|
.map((entry) => entry.m);
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
223
|
+
// Silent renditions rank BELOW every muxed one regardless of resolution. A
|
|
224
|
+
// YouTube lookup offers 2160p/1440p/1080p/… as video-only DASH streams and
|
|
225
|
+
// only ~360p muxed; taking the tallest yields a clip library with no audio,
|
|
226
|
+
// which is worse than a smaller one that sounds right. A video-only rendition
|
|
227
|
+
// is still kept as a last tier — a silent clip beats a failed ingest.
|
|
228
|
+
const silent = (m) => m.hasAudioTrack === false;
|
|
229
|
+
const mp4s = list.filter(isPlayableMp4);
|
|
230
|
+
const others = list.filter((m) => !isPlayableMp4(m) && isPlayableVideo(m));
|
|
231
|
+
return [
|
|
232
|
+
...rankByHeight(mp4s.filter((m) => !silent(m))),
|
|
233
|
+
...rankByHeight(others.filter((m) => !silent(m))),
|
|
234
|
+
...rankByHeight(mp4s.filter(silent)),
|
|
235
|
+
...rankByHeight(others.filter(silent))
|
|
236
|
+
];
|
|
94
237
|
}
|
|
95
238
|
/**
|
|
96
239
|
* Pick the best rendition to ingest: the highest-resolution playable MP4, with a
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
# Google News to Video — reusable prompt & method
|
|
2
|
+
|
|
3
|
+
Turns **a recent real event → a timely short video**, using two searches instead of one.
|
|
4
|
+
|
|
5
|
+
Google News is excellent at finding **stories**. It is bad at finding **footage** — it returns
|
|
6
|
+
articles. So the method splits in two, and the split is the whole point:
|
|
7
|
+
|
|
8
|
+
1. **Stage 1 — discover the STORY** (`news-search`): what happened, when, who, where, and is it
|
|
9
|
+
worth 60 seconds.
|
|
10
|
+
2. **Stage 2 — find the VISUALS** (`video-search`, `image-search`, the free media catalog): the
|
|
11
|
+
official footage, the press conference, the eyewitness clip, the generic B-roll that fills gaps.
|
|
12
|
+
|
|
13
|
+
Searching for news and footage **at the same time** produces poor results for both. Keep the stages apart.
|
|
14
|
+
|
|
15
|
+
> ## ⚠️ Read this first
|
|
16
|
+
>
|
|
17
|
+
> **A publicly reachable video is not a licensed video.** A TikTok, a YouTube upload, or a news
|
|
18
|
+
> clip appearing in a search result grants you nothing. TikTok supports **embedding** an original
|
|
19
|
+
> post with attribution; downloading and republishing it needs permission or a defensible
|
|
20
|
+
> copyright exception. Treat every link as unlicensed until you check.
|
|
21
|
+
>
|
|
22
|
+
> For commercial client work, in order of preference: **public domain → CC0 → CC BY (with credit)
|
|
23
|
+
> → stock with an explicit commercial licence → written permission from the creator.** Everything
|
|
24
|
+
> else is a lead, not an asset.
|
|
25
|
+
>
|
|
26
|
+
> **This is a GENERAL METHOD, not a fixed script.** The queries below are formulas. Re-derive the
|
|
27
|
+
> exact strings for your topic, region, and moment.
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## The three calls
|
|
32
|
+
|
|
33
|
+
All three are synchronous, **paid plans only**, and cost a flat **$0.0003 per call** whatever the
|
|
34
|
+
result count — so ask for a **wide page once** rather than paging twice.
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
vidfarm news-search "AI video startup funding" --fresh w --limit 25
|
|
38
|
+
vidfarm video-search "warehouse robot demonstration footage" --limit 40
|
|
39
|
+
vidfarm image-search "Manila flooding press conference" --limit 40
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
REST twins: `GET /api/v1/primitives/news-search`, `/video-search`, `/image-search`
|
|
43
|
+
(`?q=…&max_results=…®ion=…&timelimit=…`). POST with a JSON body works identically.
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## Stage 1 — discover the story
|
|
48
|
+
|
|
49
|
+
### Query formula
|
|
50
|
+
|
|
51
|
+
```
|
|
52
|
+
[subject] + [event/action] + [location] + [freshness clue]
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Examples:
|
|
56
|
+
|
|
57
|
+
```
|
|
58
|
+
AI startup funding announced today
|
|
59
|
+
factory opening Philippines
|
|
60
|
+
viral product launch this week
|
|
61
|
+
robot delivery testing Toronto
|
|
62
|
+
TikTok creator economy latest
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Narrow the time window with `--fresh d|w|m|y` (`timelimit`) rather than words like "today" —
|
|
66
|
+
the freshness parameter is reliable, the word is not.
|
|
67
|
+
|
|
68
|
+
### Operators that work
|
|
69
|
+
|
|
70
|
+
```
|
|
71
|
+
"exact phrase"
|
|
72
|
+
site:domain.com
|
|
73
|
+
-keyword
|
|
74
|
+
-site:domain.com
|
|
75
|
+
OR
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
```
|
|
79
|
+
"AI video editing" startup
|
|
80
|
+
"virtual assistants" Philippines -jobs
|
|
81
|
+
TikTok creator fund OR monetization
|
|
82
|
+
OpenAI video announcement -Reddit
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### Trusted sources
|
|
86
|
+
|
|
87
|
+
```
|
|
88
|
+
site:reuters.com artificial intelligence video
|
|
89
|
+
site:apnews.com Philippines technology
|
|
90
|
+
site:techcrunch.com creator economy
|
|
91
|
+
site:theverge.com TikTok editing
|
|
92
|
+
site:newsroom.tiktok.com creators
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### Primary sources (where the real footage usually is)
|
|
96
|
+
|
|
97
|
+
```
|
|
98
|
+
site:youtube.com official [event]
|
|
99
|
+
site:newsroom.company.com [event]
|
|
100
|
+
site:gov.ph [event]
|
|
101
|
+
site:*.gov press conference [topic]
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### What to collect per story
|
|
105
|
+
|
|
106
|
+
- Headline
|
|
107
|
+
- Publication time **and** event date (they differ, and the event date is the one that matters)
|
|
108
|
+
- People / company / location
|
|
109
|
+
- Primary source URL
|
|
110
|
+
- **Two independent** reporting sources
|
|
111
|
+
|
|
112
|
+
---
|
|
113
|
+
|
|
114
|
+
## Stage 2 — find the visuals
|
|
115
|
+
|
|
116
|
+
### Stories that come with video
|
|
117
|
+
|
|
118
|
+
Add media words to the story, not to the topic:
|
|
119
|
+
|
|
120
|
+
```
|
|
121
|
+
[story] video
|
|
122
|
+
[story] footage
|
|
123
|
+
[story] caught on camera
|
|
124
|
+
[story] press conference
|
|
125
|
+
[story] demonstration
|
|
126
|
+
[story] eyewitness video
|
|
127
|
+
[story] livestream
|
|
128
|
+
[story] official footage
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
```
|
|
132
|
+
Manila flooding eyewitness video
|
|
133
|
+
warehouse robot demonstration footage
|
|
134
|
+
new smartphone launch press conference
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
**Local TV stations often carry better raw visuals than large written publications:**
|
|
138
|
+
|
|
139
|
+
```
|
|
140
|
+
[location] [event] local news video
|
|
141
|
+
[location] [event] TV footage
|
|
142
|
+
site:youtube.com [location] [event] news
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
### The four searches to run for every story
|
|
146
|
+
|
|
147
|
+
```
|
|
148
|
+
"[company or event]" official video
|
|
149
|
+
"[person]" press conference footage
|
|
150
|
+
"[location]" raw footage
|
|
151
|
+
site:youtube.com "[exact event]"
|
|
152
|
+
site:tiktok.com "[exact event]"
|
|
153
|
+
site:pexels.com/videos [generic visual]
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### TikTok
|
|
157
|
+
|
|
158
|
+
Regular Google search beats Google News for TikTok:
|
|
159
|
+
|
|
160
|
+
```
|
|
161
|
+
site:tiktok.com "exact event"
|
|
162
|
+
site:tiktok.com/@*/video/ "company name"
|
|
163
|
+
site:tiktok.com "Manila flooding" today
|
|
164
|
+
site:tiktok.com "new robot" demonstration
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
Use a TikTok as: a **lead** pointing at an event · a **social reaction** shown through the official
|
|
168
|
+
embed · a **style reference** for the edit · **footage only** with the creator's permission.
|
|
169
|
+
|
|
170
|
+
Google does not index every TikTok. When you want breadth, TikTok's own in-app search is better;
|
|
171
|
+
Google wins when you want one exact phrase or creator.
|
|
172
|
+
|
|
173
|
+
### Filling the gaps
|
|
174
|
+
|
|
175
|
+
Whatever the story does not supply, take from licensed stock — the free catalog first:
|
|
176
|
+
|
|
177
|
+
```bash
|
|
178
|
+
vidfarm media search "city traffic night" --type video # Pixabay / Openverse, $0, licence attached
|
|
179
|
+
vidfarm public-raws --category b-roll # the platform's own free shelf
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
---
|
|
183
|
+
|
|
184
|
+
## The pipeline
|
|
185
|
+
|
|
186
|
+
```
|
|
187
|
+
news-search → video-search / image-search → licensed stock fills the gaps
|
|
188
|
+
(story) (original visuals) (generic shots)
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
Then the normal Vidfarm path: `vidfarm raws scan <url>` to mine clips out of a source you are
|
|
192
|
+
entitled to use, `vidfarm download-video <url>` to collect one file, fork a template, edit, render.
|
|
193
|
+
|
|
194
|
+
---
|
|
195
|
+
|
|
196
|
+
## Copy-paste agent prompt
|
|
197
|
+
|
|
198
|
+
```
|
|
199
|
+
Search Google News for stories about [TOPIC] published within the last [TIME WINDOW].
|
|
200
|
+
|
|
201
|
+
Prioritize:
|
|
202
|
+
1. Stories with strong visual potential
|
|
203
|
+
2. Events with official videos, demonstrations, press conferences or eyewitness footage
|
|
204
|
+
3. Primary sources and at least two independent reports
|
|
205
|
+
4. Stories that can be explained accurately in under 60 seconds
|
|
206
|
+
|
|
207
|
+
For each story, return:
|
|
208
|
+
- Headline and event date
|
|
209
|
+
- One-sentence summary
|
|
210
|
+
- Why it is visually interesting
|
|
211
|
+
- Primary source
|
|
212
|
+
- Two corroborating sources
|
|
213
|
+
- Five suggested B-roll searches
|
|
214
|
+
- Official footage links, if available
|
|
215
|
+
- Relevant YouTube and TikTok search queries
|
|
216
|
+
- Footage licensing or permission status
|
|
217
|
+
|
|
218
|
+
Do not treat a publicly accessible video as licensed for reuse.
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
---
|
|
222
|
+
|
|
223
|
+
## Accuracy rules for the video itself
|
|
224
|
+
|
|
225
|
+
Timely content is the fastest way to be publicly wrong. Three rules:
|
|
226
|
+
|
|
227
|
+
1. **Two independent sources or it does not go on screen.** One outlet reporting a claim is a
|
|
228
|
+
claim, not a fact.
|
|
229
|
+
2. **Date the event on screen** when the story is developing. "As of [date]" costs one line and
|
|
230
|
+
protects the video when the story moves.
|
|
231
|
+
3. **Attribute footage in-frame** when the licence asks for it, and never imply an organisation or
|
|
232
|
+
person endorses you because their clip appears.
|
|
233
|
+
|
|
234
|
+
The hook/loop/payoff standard still applies — a news peg is a **reason to watch now**, not a
|
|
235
|
+
substitute for a hook.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@officexapp/vidfarm-devcli",
|
|
3
|
-
"version": "0.21.
|
|
3
|
+
"version": "0.21.45",
|
|
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": {
|
|
@@ -104,15 +104,18 @@
|
|
|
104
104
|
"start": "node --import ./dist/src/instrument.js --enable-source-maps dist/src/index.js",
|
|
105
105
|
"check": "tsc -p tsconfig.json --noEmit && npm run check:skills",
|
|
106
106
|
"test:clips": "node --import tsx --test test/clip-curation.test.ts",
|
|
107
|
+
"test:social-download": "node --import tsx --test test/video-download-media.test.ts test/social-download-lookup.test.ts",
|
|
107
108
|
"test:qa": "node --import tsx --test test/qa-check.test.ts",
|
|
108
109
|
"test:studio-brand": "node --import tsx --test test/studio-brand.test.ts",
|
|
109
110
|
"test:stickers": "node --import tsx --test test/sticker-pack.test.ts test/plate-key.test.ts",
|
|
110
111
|
"test:social-recycle": "node --import tsx --test test/social-recycle.test.ts",
|
|
111
112
|
"test:dedupe": "node --import tsx --test test/dedupe-recipe.test.ts",
|
|
112
113
|
"test:iconscout": "node --import tsx --test test/iconscout.test.ts",
|
|
114
|
+
"test:web-search": "node --import tsx --test test/web-search.test.ts",
|
|
113
115
|
"test:billing": "node --import tsx --test test/billing-wallet-cas.test.ts",
|
|
114
116
|
"test:skill-docs": "node --import tsx --test test/skill-docs.test.ts",
|
|
115
117
|
"test:consult": "node --import tsx --test test/consult.test.ts",
|
|
118
|
+
"test:social-meta": "node --import tsx --test test/social-meta.test.ts",
|
|
116
119
|
"check:skills": "node scripts/build-director-skill-rollup.mjs --check && node scripts/check-skill-routes.mjs && node scripts/check-skill-nav.mjs",
|
|
117
120
|
"benchmark:editor-chat": "node --import tsx scripts/benchmark-editor-chat-harness.mjs",
|
|
118
121
|
"cdk:deploy:prod-serverless": "npm run build && dotenv -e .env.production -- npx aws-cdk deploy --app 'node dist/infra/cdk/bin/vidfarm-prod.js'",
|