@officexapp/vidfarm-devcli 0.21.42 → 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 +40 -14
- package/.agents/skills/vidfarm/recipes/onboard-a-new-director.md +9 -8
- package/.agents/skills/vidfarm/references/assets-and-sourcing.md +97 -1
- package/.agents/skills/vidfarm/references/automation-and-local-dev.md +12 -5
- package/.agents/skills/vidfarm/references/content-ideas.md +234 -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 +63 -2
- package/.agents/skills/vidfarm/references/primitives.md +51 -0
- package/.agents/skills/vidfarm-media/SKILL.md +2 -0
- package/SKILL.director.md +534 -41
- package/SKILL.md +117 -113
- package/crowdsourcing.md +44 -1
- package/dist/src/cli.js +714 -30
- package/dist/src/devcli/consult.js +403 -0
- package/dist/src/devcli/skill-docs.js +61 -7
- package/dist/src/services/brainstorm-prompts.js +132 -0
- 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/experimental/unique-product-explainers.md +855 -0
- package/package.json +10 -1
- package/public/assets/file-directory-app.js +28 -28
- package/public/assets/homepage-client-app.js +15 -15
- package/src/assets/SELLING_AWARENESS_STAGES.md +579 -0
- package/src/assets/SELLING_WITH_HOOKS.md +377 -0
|
@@ -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.
|