ossclip 0.1.33 → 0.1.34
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/editor-dist/assets/index-Dn813PGp.js +166 -0
- package/editor-dist/index.html +1 -1
- package/package.json +4 -4
- package/src/caption-regen-panel.ts +74 -0
- package/src/edit.ts +632 -9
- package/src/produce.ts +128 -4
- package/src/program.ts +57 -2
- package/src/publish.ts +350 -6
- package/src/render-replay-args.ts +50 -0
- package/editor-dist/assets/index-DA7vDWn_.js +0 -166
package/src/produce.ts
CHANGED
|
@@ -166,7 +166,12 @@ import {
|
|
|
166
166
|
AGY_PRINT_TIMEOUT,
|
|
167
167
|
type Production,
|
|
168
168
|
ossclipOutputPathFor,
|
|
169
|
+
resolveOutputFrame,
|
|
170
|
+
RESOLUTION_CHOICES,
|
|
171
|
+
smallestSource,
|
|
172
|
+
ResolutionChoiceSchema,
|
|
169
173
|
type ProviderName,
|
|
174
|
+
type ResolutionChoice,
|
|
170
175
|
type Scene,
|
|
171
176
|
type SceneComponentId,
|
|
172
177
|
type Segment,
|
|
@@ -256,6 +261,14 @@ export const TranscriptKeySchema = z.object({
|
|
|
256
261
|
* runs) means "no biasing".
|
|
257
262
|
*/
|
|
258
263
|
dictionary: z.array(z.string()).optional(),
|
|
264
|
+
/**
|
|
265
|
+
* Whether whisper ran its TRANSLATE task (`-tr`, 2026-08-29). It changes
|
|
266
|
+
* the decoded TEXT — Urdu speech comes back as English words — so it
|
|
267
|
+
* re-keys the cache exactly like the language does, or a warm workdir
|
|
268
|
+
* serves the Urdu-script transcript to a translate run. Absent (old key
|
|
269
|
+
* files) means "no translation", the `dictionary` contract.
|
|
270
|
+
*/
|
|
271
|
+
translate: z.boolean().optional(),
|
|
259
272
|
});
|
|
260
273
|
export type TranscriptKey = z.infer<typeof TranscriptKeySchema>;
|
|
261
274
|
|
|
@@ -280,6 +293,9 @@ export function transcriptCacheReusable(
|
|
|
280
293
|
// "" and absent both mean whisper's en default — program.ts rejects an
|
|
281
294
|
// empty code, but a key file predating that guard must not wedge.
|
|
282
295
|
(effective.language ?? "") === (requested.language ?? "") &&
|
|
296
|
+
// Absent and false are the same "no translation", so pre-flag key
|
|
297
|
+
// files reuse under a non-translate request.
|
|
298
|
+
(effective.translate ?? false) === (requested.translate ?? false) &&
|
|
283
299
|
// ORDER-SENSITIVE by choice: the dictionary becomes whisper's --prompt
|
|
284
300
|
// text verbatim, so a reordered list genuinely is a different decoder
|
|
285
301
|
// input — treating it as equal would serve a transcript biased by a
|
|
@@ -537,6 +553,13 @@ export interface ProduceOptions {
|
|
|
537
553
|
* decodes garbage (Urdu field test 2026-08-05).
|
|
538
554
|
*/
|
|
539
555
|
whisperLanguage?: string;
|
|
556
|
+
/**
|
|
557
|
+
* `--whisper-translate`: whisper's TRANSLATE task (`-tr`) — non-English
|
|
558
|
+
* speech decoded straight to ENGLISH text. Distinct from
|
|
559
|
+
* `whisperLanguage`, which says what is SPOKEN; the two are passed
|
|
560
|
+
* together (whisper decodes better knowing the source language).
|
|
561
|
+
*/
|
|
562
|
+
whisperTranslate?: boolean;
|
|
540
563
|
/**
|
|
541
564
|
* Vocabulary terms for this run (`--dictionary`, F4 2026-08-16), already
|
|
542
565
|
* split/trimmed by the action. Wholesale beats the config's `dictionary`
|
|
@@ -584,6 +607,16 @@ export interface ProduceOptions {
|
|
|
584
607
|
* platform chrome to dodge and a landscape source needs no cropping at all.
|
|
585
608
|
*/
|
|
586
609
|
aspect?: "9:16" | "16:9";
|
|
610
|
+
/**
|
|
611
|
+
* `--resolution <height>`: how big the output actually renders. `auto`
|
|
612
|
+
* keeps what the source has (capped at 2160); an explicit height scales the
|
|
613
|
+
* 1080-wide composition to it. Already validated by commander (or by
|
|
614
|
+
* `ResolutionChoiceSchema` when it comes from the config), so this is a
|
|
615
|
+
* choice, never a raw string. `resolveOutputFrame` owns the math and the
|
|
616
|
+
* why; the value reaches THREE stages that would otherwise each pin 1080p:
|
|
617
|
+
* the folder-concat target, the mezzanine, and the render's own scale.
|
|
618
|
+
*/
|
|
619
|
+
resolution?: ResolutionChoice;
|
|
587
620
|
/**
|
|
588
621
|
* `--concurrency <n>`: how many browser tabs the render opens at once,
|
|
589
622
|
* beating the config's `renderConcurrency` and the cpus-2 default
|
|
@@ -709,6 +742,32 @@ export function resolveWatermark(
|
|
|
709
742
|
return flag ?? configValue === true;
|
|
710
743
|
}
|
|
711
744
|
|
|
745
|
+
/**
|
|
746
|
+
* The effective `--resolution` — `resolveWatermark`'s precedence verbatim: a
|
|
747
|
+
* TYPED flag always wins, and only then does the config supply the default.
|
|
748
|
+
*
|
|
749
|
+
* The config side is ZOD-PARSED rather than trusted: `loadConfig` hands back
|
|
750
|
+
* whatever the hand-editable JSON held, and an unparsed `"4k"` would reach
|
|
751
|
+
* `Number("4k")` inside `resolveOutputFrame` as NaN and size the whole render
|
|
752
|
+
* off it. A malformed value earns one warning naming the key and falls back
|
|
753
|
+
* to 1080 — the value every existing run already produces, so a typo costs a
|
|
754
|
+
* message rather than a surprise 4K render (CLAUDE.md: parse, never coerce).
|
|
755
|
+
* Pure, so the flag × config matrix is testable without a config file.
|
|
756
|
+
*/
|
|
757
|
+
export function resolveResolution(
|
|
758
|
+
flag: ResolutionChoice | undefined,
|
|
759
|
+
configValue: unknown,
|
|
760
|
+
): ResolutionChoice {
|
|
761
|
+
if (flag !== undefined) return flag;
|
|
762
|
+
if (configValue === undefined) return "1080";
|
|
763
|
+
const parsed = ResolutionChoiceSchema.safeParse(configValue);
|
|
764
|
+
if (parsed.success) return parsed.data;
|
|
765
|
+
console.log(
|
|
766
|
+
`⚠ config resolution ignored — expected one of ${RESOLUTION_CHOICES.join(", ")}, using 1080`,
|
|
767
|
+
);
|
|
768
|
+
return "1080";
|
|
769
|
+
}
|
|
770
|
+
|
|
712
771
|
/**
|
|
713
772
|
* The effective `--cover-in-video` switch — resolveWatermark's semantics
|
|
714
773
|
* verbatim: a TYPED flag always wins (so `--no-cover-in-video` beats a
|
|
@@ -2078,6 +2137,46 @@ export async function produce(inputArg: string, opts: ProduceOptions): Promise<P
|
|
|
2078
2137
|
|
|
2079
2138
|
const tools = { ffmpegPath: cfg.ffmpegPath, ffprobePath: cfg.ffprobePath };
|
|
2080
2139
|
|
|
2140
|
+
// `--resolution` (2026-08-27), resolved before ANY stage that sizes pixels:
|
|
2141
|
+
// the folder concat, the mezzanine and the render each used to pin 1080p
|
|
2142
|
+
// independently, so a 4K take lost three quarters of its pixels before
|
|
2143
|
+
// anything looked at it. `auto` needs the SOURCE's size, which for a folder
|
|
2144
|
+
// means probing the clips here — the concat target is chosen before the
|
|
2145
|
+
// concatenated file (and its probe) exists.
|
|
2146
|
+
const resolution = resolveResolution(opts.resolution, cfg.resolution);
|
|
2147
|
+
const autoSource = async (): Promise<{ width: number; height: number } | null> => {
|
|
2148
|
+
if (resolution !== "auto") return null;
|
|
2149
|
+
if (isFolder && folderListing) {
|
|
2150
|
+
// Metadata-only probes, and only under `auto`: the default path adds no
|
|
2151
|
+
// ffprobe calls at all (the 4m32s probe-storm lesson, concat.ts:272).
|
|
2152
|
+
const sizes: Array<{ width: number; height: number }> = [];
|
|
2153
|
+
for (const entry of folderListing.entries) {
|
|
2154
|
+
try {
|
|
2155
|
+
const p = await probe(tools, join(input, entry.name));
|
|
2156
|
+
sizes.push({ width: p.width, height: p.height });
|
|
2157
|
+
} catch {
|
|
2158
|
+
// A clip that will not probe is the concat guard's problem, not
|
|
2159
|
+
// this sizing pass's — skip it rather than fail the run here.
|
|
2160
|
+
}
|
|
2161
|
+
}
|
|
2162
|
+
return smallestSource(sizes);
|
|
2163
|
+
}
|
|
2164
|
+
try {
|
|
2165
|
+
const p = await probe(tools, input);
|
|
2166
|
+
return { width: p.width, height: p.height };
|
|
2167
|
+
} catch {
|
|
2168
|
+
return null;
|
|
2169
|
+
}
|
|
2170
|
+
};
|
|
2171
|
+
const output = resolveOutputFrame({
|
|
2172
|
+
frame,
|
|
2173
|
+
source: (await autoSource()) ?? { width: 0, height: 0 },
|
|
2174
|
+
resolution,
|
|
2175
|
+
});
|
|
2176
|
+
if (output.scale !== 1) {
|
|
2177
|
+
console.log(`▸ resolution: ${output.width}x${output.height} (${resolution})`);
|
|
2178
|
+
}
|
|
2179
|
+
|
|
2081
2180
|
// Resolved ONCE for the whole run — whisper biasing, repair vouching and
|
|
2082
2181
|
// caption casing must all see the same list, or the passes disagree about
|
|
2083
2182
|
// what a term is spelled like. A typed --dictionary wholesale beats the
|
|
@@ -2182,9 +2281,12 @@ export async function produce(inputArg: string, opts: ProduceOptions): Promise<P
|
|
|
2182
2281
|
if (isFolder && folderListing) {
|
|
2183
2282
|
const sort = opts.sort ?? "name";
|
|
2184
2283
|
const result = await phases.time("ffmpeg", () =>
|
|
2284
|
+
// The concat target carries the RESOLVED size, not the base frame:
|
|
2285
|
+
// letterboxing every take into 1080p here would throw the pixels away
|
|
2286
|
+
// before the mezzanine or the render ever saw them (`--resolution`).
|
|
2185
2287
|
concatFolder(tools, input, folderListing!, work, sort, {
|
|
2186
|
-
w:
|
|
2187
|
-
h:
|
|
2288
|
+
w: output.width,
|
|
2289
|
+
h: output.height,
|
|
2188
2290
|
}),
|
|
2189
2291
|
);
|
|
2190
2292
|
console.log(
|
|
@@ -2304,6 +2406,9 @@ export async function produce(inputArg: string, opts: ProduceOptions): Promise<P
|
|
|
2304
2406
|
const requestedKey: TranscriptKey = {
|
|
2305
2407
|
model: requestedModel,
|
|
2306
2408
|
...(whisperLang.language !== undefined ? { language: whisperLang.language } : {}),
|
|
2409
|
+
// Omitted when off, so a non-translate run's key stays byte-identical to
|
|
2410
|
+
// every pre-flag key file (the dictionary posture).
|
|
2411
|
+
...(opts.whisperTranslate === true ? { translate: true } : {}),
|
|
2307
2412
|
// Omitted when empty, not written as [] — pre-dictionary key files have
|
|
2308
2413
|
// no `dictionary` at all, and transcriptCacheReusable reads absent and
|
|
2309
2414
|
// empty as the same "no biasing", so old workdirs must not re-transcribe.
|
|
@@ -2374,6 +2479,9 @@ export async function produce(inputArg: string, opts: ProduceOptions): Promise<P
|
|
|
2374
2479
|
language: requestedKey.language,
|
|
2375
2480
|
// Vocabulary biasing (F4) — undefined for an empty dictionary, so
|
|
2376
2481
|
// the spawned args stay byte-identical to every pre-dictionary run.
|
|
2482
|
+
// From the KEY, like the language: whatever re-keys the cache is
|
|
2483
|
+
// what actually ran, so the two can never disagree.
|
|
2484
|
+
...(requestedKey.translate === true ? { translate: true } : {}),
|
|
2377
2485
|
prompt: whisperPromptFor(dictionary),
|
|
2378
2486
|
},
|
|
2379
2487
|
audioPath,
|
|
@@ -3829,7 +3937,11 @@ export async function produce(inputArg: string, opts: ProduceOptions): Promise<P
|
|
|
3829
3937
|
scenes: scenes.length > 0 ? scenes : undefined,
|
|
3830
3938
|
producer: producerStamp,
|
|
3831
3939
|
theme,
|
|
3832
|
-
|
|
3940
|
+
// The EFFECTIVE output size, not the base frame (`--resolution`): this is
|
|
3941
|
+
// what the file on disk actually is, and it is read downstream by the
|
|
3942
|
+
// mezzanine's scale decision, the NLE exports and the editor — all of
|
|
3943
|
+
// which would otherwise describe a 1080p file that isn't there.
|
|
3944
|
+
render: { width: output.width, height: output.height, fps: 30 },
|
|
3833
3945
|
};
|
|
3834
3946
|
await writeFile(join(work, "production.json"), JSON.stringify(production, null, 2));
|
|
3835
3947
|
|
|
@@ -4383,7 +4495,15 @@ export async function produce(inputArg: string, opts: ProduceOptions): Promise<P
|
|
|
4383
4495
|
// ~/.ossclip/config.json's theme the first time anyone touched a color.
|
|
4384
4496
|
baseTheme: configBaseTheme,
|
|
4385
4497
|
baseCaptionLines,
|
|
4386
|
-
|
|
4498
|
+
// The COMPOSITION's size, which is the BASE frame — never
|
|
4499
|
+
// `production.render` (2026-08-27). `production.render` describes the
|
|
4500
|
+
// FILE, and under `--resolution` those differ by the render's `scale`:
|
|
4501
|
+
// Remotion enlarges this composition by that factor, so sizing the
|
|
4502
|
+
// composition from the scaled dims applies it TWICE (2160×3840 became
|
|
4503
|
+
// 4320×7680 frames, which h264_videotoolbox refuses outright, at stitch
|
|
4504
|
+
// time, after every frame had been paid for). The Player reads these too,
|
|
4505
|
+
// and previewing at the base size is what keeps the editor cheap.
|
|
4506
|
+
settings: { ...frame, fps: production.render.fps },
|
|
4387
4507
|
outputDurationSec: map.outputDuration,
|
|
4388
4508
|
// The aspect travels with the measurement because the crop math needs it:
|
|
4389
4509
|
// `object-fit: cover` spills vertically for a portrait source and
|
|
@@ -4890,6 +5010,10 @@ export async function produce(inputArg: string, opts: ProduceOptions): Promise<P
|
|
|
4890
5010
|
outPath: rawPath,
|
|
4891
5011
|
browserExecutable: cfg.browserExecutable,
|
|
4892
5012
|
concurrency: renderConcurrency.concurrency,
|
|
5013
|
+
// The composition stays 1080-wide and Remotion renders it larger
|
|
5014
|
+
// (`--resolution`) — rebuilding the stage at 2160 would keep
|
|
5015
|
+
// `captionFontSizeFor`'s absolute 64px and draw quarter-size captions.
|
|
5016
|
+
scale: output.scale,
|
|
4893
5017
|
cancelSignal: renderCancel.cancelSignal,
|
|
4894
5018
|
onPhase: (phase: RenderPhase) => {
|
|
4895
5019
|
signalPhase = renderSignalPhaseOf(phase);
|
package/src/program.ts
CHANGED
|
@@ -3,14 +3,20 @@ import { existsSync, readFileSync } from "node:fs";
|
|
|
3
3
|
import { dirname, join, resolve } from "node:path";
|
|
4
4
|
import { Command, InvalidArgumentError } from "commander";
|
|
5
5
|
import { z } from "zod/v4";
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
CleanupLevelSchema,
|
|
8
|
+
COVER_MAX_WORDS,
|
|
9
|
+
RESOLUTION_CHOICES,
|
|
10
|
+
ResolutionChoiceSchema,
|
|
11
|
+
SceneComponentIdSchema,
|
|
12
|
+
} from "@ossclip/core";
|
|
7
13
|
import { STUDIO_ENTRY } from "@ossclip/renderer";
|
|
8
14
|
import { loadEnvFiles } from "./env";
|
|
9
15
|
import { ExportFormatSchema, runAnalyze } from "./analyze";
|
|
10
16
|
import { expandHome } from "./paths";
|
|
11
17
|
import { phaseBucketProps } from "./phase-timing";
|
|
12
18
|
import { dictionaryFlag, jumpCutsFlag, produce, reviewFlag } from "./produce";
|
|
13
|
-
import { accountsFlag, atFlag, platformsFlag } from "./publish";
|
|
19
|
+
import { accountsFlag, atFlag, deliveryFlag, platformsFlag, youtubePrivacyFlag } from "./publish";
|
|
14
20
|
// The one interactive import that is STATIC rather than `await import()`: the
|
|
15
21
|
// `resetInputSource()` run boundary in `buildProgram` has to run synchronously
|
|
16
22
|
// while the program is being built, and `buildProgram` cannot await. The graph
|
|
@@ -300,6 +306,22 @@ export function buildProgram(): Command {
|
|
|
300
306
|
"output shape: 9:16 (vertical, default) or 16:9 (landscape, 1920x1080)",
|
|
301
307
|
"9:16",
|
|
302
308
|
)
|
|
309
|
+
.option(
|
|
310
|
+
"--resolution <height>",
|
|
311
|
+
"output height: 1080 (default), 1440, 2160, or auto (keep what the source has, " +
|
|
312
|
+
"capped at 2160). Config key: \"resolution\"",
|
|
313
|
+
(v: string) => {
|
|
314
|
+
// Parse, never coerce (CLAUDE.md): a typo'd `--resolution 2610` must
|
|
315
|
+
// not silently render 1080p, and `Number()` on "auto" is NaN.
|
|
316
|
+
const parsed = ResolutionChoiceSchema.safeParse(v.trim());
|
|
317
|
+
if (!parsed.success) {
|
|
318
|
+
throw new InvalidArgumentError(
|
|
319
|
+
`--resolution wants one of ${RESOLUTION_CHOICES.join(", ")}, got "${v}"`,
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
return parsed.data;
|
|
323
|
+
},
|
|
324
|
+
)
|
|
303
325
|
.option("--produce", "run the LLM producer brain to plan title cards & graphics", false)
|
|
304
326
|
.option(
|
|
305
327
|
"--clip <seconds>",
|
|
@@ -357,6 +379,12 @@ export function buildProgram(): Command {
|
|
|
357
379
|
"--whisper-language <code>",
|
|
358
380
|
"transcription language code for a multilingual model, e.g. ur | de | auto (whisper defaults to en)",
|
|
359
381
|
)
|
|
382
|
+
.option(
|
|
383
|
+
"--whisper-translate",
|
|
384
|
+
"translate the speech to ENGLISH captions instead of transcribing it verbatim " +
|
|
385
|
+
"(whisper's -tr; pair with --whisper-language for the SOURCE language)",
|
|
386
|
+
false,
|
|
387
|
+
)
|
|
360
388
|
// COMMA-SEPARATED in one value, not variadic: a variadic option swallows
|
|
361
389
|
// the optional positional [input] whenever the flag precedes the path,
|
|
362
390
|
// and commander offers no way to give the positional priority.
|
|
@@ -644,6 +672,7 @@ export function buildProgram(): Command {
|
|
|
644
672
|
repair: opts.repair,
|
|
645
673
|
whisperModel: opts.whisperModel,
|
|
646
674
|
whisperLanguage,
|
|
675
|
+
whisperTranslate: opts.whisperTranslate === true,
|
|
647
676
|
// Split/trim/drop-empties (dictionaryFlag) — undefined stays
|
|
648
677
|
// undefined so the config's dictionary can supply the default.
|
|
649
678
|
dictionary: dictionaryFlag(opts.dictionary),
|
|
@@ -687,6 +716,9 @@ export function buildProgram(): Command {
|
|
|
687
716
|
// Validated by concurrencyFlag at parse time; undefined = "not
|
|
688
717
|
// typed", which is what lets the config supply it.
|
|
689
718
|
concurrency: opts.concurrency,
|
|
719
|
+
// Same contract: validated by the flag's own parser, and undefined
|
|
720
|
+
// means "not typed" so the config's `resolution` can supply it.
|
|
721
|
+
resolution: opts.resolution,
|
|
690
722
|
});
|
|
691
723
|
// Counts, buckets and names only — the duration crosses the wire as a
|
|
692
724
|
// bucket, and nothing here can carry a path (assertSafeProps enforces
|
|
@@ -758,6 +790,12 @@ export function buildProgram(): Command {
|
|
|
758
790
|
"--whisper-language <code>",
|
|
759
791
|
"transcription language code for a multilingual model, e.g. ur | de | auto (whisper defaults to en)",
|
|
760
792
|
)
|
|
793
|
+
.option(
|
|
794
|
+
"--whisper-translate",
|
|
795
|
+
"translate the speech to ENGLISH captions instead of transcribing it verbatim " +
|
|
796
|
+
"(whisper's -tr; pair with --whisper-language for the SOURCE language)",
|
|
797
|
+
false,
|
|
798
|
+
)
|
|
761
799
|
.action(async (input: string, opts) => {
|
|
762
800
|
const cleanup = CleanupLevelSchema.parse(opts.cleanup);
|
|
763
801
|
const result = await produce(input, {
|
|
@@ -769,6 +807,7 @@ export function buildProgram(): Command {
|
|
|
769
807
|
noiseDb: opts.noiseDb,
|
|
770
808
|
whisperModel: opts.whisperModel,
|
|
771
809
|
// Same guard as produce's: empty must error, not become a bare `-l`.
|
|
810
|
+
whisperTranslate: opts.whisperTranslate === true,
|
|
772
811
|
whisperLanguage:
|
|
773
812
|
opts.whisperLanguage !== undefined
|
|
774
813
|
? z.string().trim().min(1, "--whisper-language needs a code, e.g. ur").parse(opts.whisperLanguage)
|
|
@@ -1035,6 +1074,18 @@ export function buildProgram(): Command {
|
|
|
1035
1074
|
.option("--dry-run", "print the targets and the exact payload; send nothing", false)
|
|
1036
1075
|
.option("-y, --yes", "skip the confirmation prompt", false)
|
|
1037
1076
|
.option("--force", "publish again even though this workdir already has a publish receipt", false)
|
|
1077
|
+
.option(
|
|
1078
|
+
"--youtube-privacy <level>",
|
|
1079
|
+
"YouTube visibility: private (default), unlisted or public",
|
|
1080
|
+
youtubePrivacyFlag,
|
|
1081
|
+
)
|
|
1082
|
+
.option(
|
|
1083
|
+
"--delivery <mode>",
|
|
1084
|
+
"what to upload — auto: a ≤1080p ~10 Mbps delivery encode, built once and " +
|
|
1085
|
+
"cached in the workdir; master: the untouched render",
|
|
1086
|
+
deliveryFlag,
|
|
1087
|
+
"auto",
|
|
1088
|
+
)
|
|
1038
1089
|
.action(async (workdir: string | undefined, opts) => {
|
|
1039
1090
|
const { runPublish } = await import("./publish");
|
|
1040
1091
|
const target = await resolveWorkdirArgument(workdir ?? ".", "publish");
|
|
@@ -1046,6 +1097,10 @@ export function buildProgram(): Command {
|
|
|
1046
1097
|
dryRun: opts.dryRun,
|
|
1047
1098
|
yes: opts.yes,
|
|
1048
1099
|
force: opts.force,
|
|
1100
|
+
// Validated by youtubePrivacyFlag at parse time; undefined means the
|
|
1101
|
+
// payload's own safe default (private).
|
|
1102
|
+
youtubePrivacy: opts.youtubePrivacy,
|
|
1103
|
+
delivery: opts.delivery,
|
|
1049
1104
|
});
|
|
1050
1105
|
telemetry.record("publish_run", {
|
|
1051
1106
|
scheduled: opts.at !== undefined,
|