@officexapp/vidfarm-devcli 0.21.43 → 0.21.46

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.
Files changed (49) hide show
  1. package/.agents/skills/editor-capabilities/SKILL.md +4 -0
  2. package/.agents/skills/vidfarm/SKILL.md +95 -17
  3. package/.agents/skills/vidfarm/harnesses/explainer.HARNESS.md +1 -1
  4. package/.agents/skills/vidfarm/harnesses/product-demo.HARNESS.md +2 -0
  5. package/.agents/skills/vidfarm/harnesses/short-form.HARNESS.md +1 -0
  6. package/.agents/skills/vidfarm/recipes/local-edit-render-approve.md +1 -1
  7. package/.agents/skills/vidfarm/recipes/onboard-a-new-director.md +1 -1
  8. package/.agents/skills/vidfarm/references/agent-included-imagegen.md +75 -0
  9. package/.agents/skills/vidfarm/references/assets-and-sourcing.md +152 -2
  10. package/.agents/skills/vidfarm/references/automation-and-local-dev.md +22 -9
  11. package/.agents/skills/vidfarm/references/browser-harness.md +93 -0
  12. package/.agents/skills/vidfarm/references/content-ideas.md +232 -10
  13. package/.agents/skills/vidfarm/references/core-workflows.md +11 -1
  14. package/.agents/skills/vidfarm/references/editor-workflows.md +39 -0
  15. package/.agents/skills/vidfarm/references/onboarding.md +1 -1
  16. package/.agents/skills/vidfarm/references/primitives.md +51 -0
  17. package/.agents/skills/vidfarm-media/SKILL.md +2 -0
  18. package/SKILL.director.md +775 -42
  19. package/SKILL.md +157 -115
  20. package/crowdsourcing.md +417 -3
  21. package/dist/src/cli.js +750 -34
  22. package/dist/src/devcli/agent-imagegen.js +181 -0
  23. package/dist/src/devcli/browser-harness.js +384 -0
  24. package/dist/src/devcli/clip-store.js +41 -3
  25. package/dist/src/devcli/consult.js +14 -0
  26. package/dist/src/devcli/cost-mode.js +23 -3
  27. package/dist/src/devcli/doctor.js +52 -3
  28. package/dist/src/devcli/hyperframes-cli.js +11 -1
  29. package/dist/src/devcli/local-render.js +4 -7
  30. package/dist/src/devcli/marketplace-gigs.js +623 -0
  31. package/dist/src/devcli/qa-check.js +89 -1
  32. package/dist/src/devcli/shared-folder.js +387 -0
  33. package/dist/src/devcli/skill-docs.js +61 -7
  34. package/dist/src/devcli/stills.js +4 -8
  35. package/dist/src/lib/ffprobe-path.js +64 -0
  36. package/dist/src/lib/render-media-prep.js +2 -11
  37. package/dist/src/services/clip-curation/ffmpeg.js +4 -15
  38. package/dist/src/services/clip-curation/index.js +1 -1
  39. package/dist/src/services/clip-curation/local-agent.js +6 -2
  40. package/dist/src/services/clip-curation/media-select.js +146 -3
  41. package/experimental/google-news-to-video.md +235 -0
  42. package/package.json +8 -150
  43. package/public/assets/file-directory-app.js +35 -35
  44. package/public/assets/homepage-client-app.js +15 -15
  45. package/public/serve-shells/library-files.html +5 -1
  46. package/public/serve-shells/library-raws.html +10 -1
  47. package/public/serve-shells/tools-clipper.html +5 -1
  48. package/public/serve-shells/tools-image.html +5 -1
  49. package/public/serve-shells/tools-video.html +5 -1
@@ -0,0 +1,64 @@
1
+ // Single source of truth for the bundled ffprobe binary path.
2
+ //
3
+ // Why this module exists: the previous dependency, `ffprobe-static@3`, ships
4
+ // binaries for EVERY platform in one tarball (335 MB unpacked — darwin 132 MB +
5
+ // linux 99 MB + win32 104 MB). Every `npm i -g @officexapp/vidfarm-devcli` paid
6
+ // for all three. On Windows that cost is multiplied again by Defender scanning
7
+ // each extracted file, which is the single biggest first-run setup delay users
8
+ // report. `@ffprobe-installer/ffprobe` declares one optional dependency per
9
+ // platform, so npm installs ONLY the matching binary (~17 MB).
10
+ //
11
+ // Resolution order (same shape as resolveFfmpeg in services/clip-curation):
12
+ // 1. HYPERFRAMES_FFPROBE_PATH / FFPROBE_PATH env override
13
+ // 2. @ffprobe-installer/ffprobe (per-platform, the bundled default)
14
+ // 3. ffprobe-static (legacy; only if something still installs it)
15
+ // 4. null → the caller falls back to a bare `ffprobe` on PATH
16
+ //
17
+ // Windows-on-ARM note: neither ffprobe-installer nor ffmpeg-static publishes a
18
+ // win32-arm64 build, so steps 2/3 return null there and PATH is the only route.
19
+ // That is not a regression — ffmpeg-static already had the same gap.
20
+ import { existsSync } from "node:fs";
21
+ import { createRequire } from "node:module";
22
+ const requireFrom = createRequire(import.meta.url);
23
+ let cached;
24
+ /** Pull `.path` off whichever module shape the installer package exports. */
25
+ function readPath(mod) {
26
+ if (typeof mod === "string")
27
+ return mod;
28
+ if (mod && typeof mod === "object") {
29
+ const direct = mod.path;
30
+ if (typeof direct === "string" && direct)
31
+ return direct;
32
+ const viaDefault = mod.default?.path;
33
+ if (typeof viaDefault === "string" && viaDefault)
34
+ return viaDefault;
35
+ }
36
+ return null;
37
+ }
38
+ /**
39
+ * Absolute path to a bundled ffprobe, or null when none is installed for this
40
+ * platform. Cached — resolution is a require() plus a stat.
41
+ */
42
+ export function resolveBundledFfprobe() {
43
+ if (cached !== undefined)
44
+ return cached;
45
+ const envPath = process.env.HYPERFRAMES_FFPROBE_PATH?.trim() || process.env.FFPROBE_PATH?.trim();
46
+ if (envPath && existsSync(envPath))
47
+ return (cached = envPath);
48
+ for (const pkg of ["@ffprobe-installer/ffprobe", "ffprobe-static"]) {
49
+ try {
50
+ const resolved = readPath(requireFrom(pkg));
51
+ if (resolved && existsSync(resolved))
52
+ return (cached = resolved);
53
+ }
54
+ catch {
55
+ // not installed for this platform — try the next candidate
56
+ }
57
+ }
58
+ return (cached = null);
59
+ }
60
+ /** Same as resolveBundledFfprobe, but falls back to a bare `ffprobe` on PATH. */
61
+ export function resolveFfprobeCommand() {
62
+ return resolveBundledFfprobe() ?? "ffprobe";
63
+ }
64
+ //# sourceMappingURL=ffprobe-path.js.map
@@ -32,6 +32,7 @@ import path from "node:path";
32
32
  import { Readable, Transform } from "node:stream";
33
33
  import { pipeline } from "node:stream/promises";
34
34
  import { parseHTML } from "linkedom";
35
+ import { resolveFfprobeCommand } from "./ffprobe-path.js";
35
36
  // Trailing pad added to every cut so the renderer never runs out of media on
36
37
  // the final frame of a clip (frame quantization + audio priming).
37
38
  const SEGMENT_PAD_SECONDS = 0.5;
@@ -71,17 +72,7 @@ async function resolveFfmpegPath() {
71
72
  return "ffmpeg";
72
73
  }
73
74
  async function resolveFfprobePath() {
74
- const env = process.env.HYPERFRAMES_FFPROBE_PATH?.trim();
75
- if (env && existsSync(env))
76
- return env;
77
- try {
78
- const mod = (await import("ffprobe-static"));
79
- const resolved = (mod.path ?? mod.default?.path);
80
- if (typeof resolved === "string" && resolved && existsSync(resolved))
81
- return resolved;
82
- }
83
- catch { /* PATH fallback */ }
84
- return "ffprobe";
75
+ return resolveFfprobeCommand();
85
76
  }
86
77
  function runCommand(bin, args, timeoutMs) {
87
78
  return new Promise((resolve, reject) => {
@@ -2,10 +2,11 @@
2
2
  // thumbnail extraction, per-scene audio for ASR. Same flags on devcli (local
3
3
  // ffmpeg-static) and cloud (bundled ffmpeg-static in the scan Lambda), so both
4
4
  // targets cut identical clips. Resolution order matches the rest of the repo:
5
- // HYPERFRAMES_FFMPEG_PATH/FFPROBE env → ffmpeg-static/ffprobe-static → PATH.
5
+ // HYPERFRAMES_FFMPEG_PATH/FFPROBE env → ffmpeg-static/@ffprobe-installer → PATH.
6
6
  import { spawn } from "node:child_process";
7
7
  import { existsSync, mkdirSync } from "node:fs";
8
8
  import path from "node:path";
9
+ import { resolveBundledFfprobe } from "../../lib/ffprobe-path.js";
9
10
  let cachedFfmpeg;
10
11
  let cachedFfprobe;
11
12
  export async function resolveFfmpeg() {
@@ -29,20 +30,8 @@ export async function resolveFfmpeg() {
29
30
  export async function resolveFfprobe() {
30
31
  if (cachedFfprobe !== undefined)
31
32
  return cachedFfprobe ?? "ffprobe";
32
- const envPath = process.env.HYPERFRAMES_FFPROBE_PATH?.trim() ?? process.env.FFPROBE_PATH?.trim();
33
- if (envPath && existsSync(envPath))
34
- return (cachedFfprobe = envPath);
35
- try {
36
- const mod = (await import("ffprobe-static"));
37
- const p = (mod.path ?? mod.default?.path);
38
- if (typeof p === "string" && p && existsSync(p))
39
- return (cachedFfprobe = p);
40
- }
41
- catch {
42
- /* fall through to PATH */
43
- }
44
- cachedFfprobe = null;
45
- return "ffprobe";
33
+ cachedFfprobe = resolveBundledFfprobe();
34
+ return cachedFfprobe ?? "ffprobe";
46
35
  }
47
36
  /** True if a usable ffmpeg is available (bundled or on PATH). */
48
37
  export async function hasFfmpeg() {
@@ -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.
@@ -16,9 +16,13 @@ export function detectLocalAgent(preferred) {
16
16
  const candidates = preferred === "claude" ? ["claude"]
17
17
  : preferred === "codex" ? ["codex"]
18
18
  : ["claude", "codex"];
19
+ // Windows has no `which` — the equivalent is `where`, which prints one match
20
+ // per line (take the first). Without this branch, agent detection always
21
+ // reported "none installed" on Windows even with Claude Code on PATH.
22
+ const lookup = process.platform === "win32" ? "where" : "which";
19
23
  for (const kind of candidates) {
20
- const found = spawnSync("which", [kind], { encoding: "utf8" });
21
- const bin = (found.stdout || "").trim();
24
+ const found = spawnSync(lookup, [kind], { encoding: "utf8", shell: process.platform === "win32" });
25
+ const bin = (found.stdout || "").split(/\r?\n/)[0]?.trim() ?? "";
22
26
  if (found.status === 0 && bin)
23
27
  return { kind, bin };
24
28
  }
@@ -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
- const mp4s = rankByHeight(list.filter(isPlayableMp4));
92
- const others = rankByHeight(list.filter((m) => !isPlayableMp4(m) && isPlayableVideo(m)));
93
- return [...mp4s, ...others];
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=…&region=…&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.