ossclip 0.1.31 → 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/src/program.ts CHANGED
@@ -3,13 +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 { CleanupLevelSchema, COVER_MAX_WORDS, SceneComponentIdSchema } from "@ossclip/core";
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";
19
+ import { accountsFlag, atFlag, deliveryFlag, platformsFlag, youtubePrivacyFlag } from "./publish";
13
20
  // The one interactive import that is STATIC rather than `await import()`: the
14
21
  // `resetInputSource()` run boundary in `buildProgram` has to run synchronously
15
22
  // while the program is being built, and `buildProgram` cannot await. The graph
@@ -299,6 +306,22 @@ export function buildProgram(): Command {
299
306
  "output shape: 9:16 (vertical, default) or 16:9 (landscape, 1920x1080)",
300
307
  "9:16",
301
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
+ )
302
325
  .option("--produce", "run the LLM producer brain to plan title cards & graphics", false)
303
326
  .option(
304
327
  "--clip <seconds>",
@@ -356,6 +379,12 @@ export function buildProgram(): Command {
356
379
  "--whisper-language <code>",
357
380
  "transcription language code for a multilingual model, e.g. ur | de | auto (whisper defaults to en)",
358
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
+ )
359
388
  // COMMA-SEPARATED in one value, not variadic: a variadic option swallows
360
389
  // the optional positional [input] whenever the flag precedes the path,
361
390
  // and commander offers no way to give the positional priority.
@@ -406,6 +435,19 @@ export function buildProgram(): Command {
406
435
  .option("--no-watermark", "no wordmark, even when the config turns it on")
407
436
  // Same tri-state shape as --watermark above (positive declared first so
408
437
  // commander's default stays undefined = "not typed"): the config's
438
+ // `coverInVideo` key supplies the default (resolveCoverInVideo), and a
439
+ // typed --no-cover-in-video still beats a config-on. A separate key from
440
+ // --cover/--no-cover, which is about WRITING the cover file at all.
441
+ .option(
442
+ "--cover-in-video",
443
+ "overlay the cover image on the video's first frames, for the platforms that ignore " +
444
+ "an uploaded cover and use frame 1. Nothing is inserted — the overlay ends at the " +
445
+ "first spoken word (max 0.5s), so no timing moves " +
446
+ "(set it once with coverInVideo: true in ~/.ossclip/config.json)",
447
+ )
448
+ .option("--no-cover-in-video", "no cover overlay, even when the config turns it on")
449
+ // Same tri-state shape as --watermark above (positive declared first so
450
+ // commander's default stays undefined = "not typed"): the config's
409
451
  // `youtube` key supplies the default (resolveYoutube), and a typed
410
452
  // --no-youtube still beats a config-on.
411
453
  .option(
@@ -630,6 +672,7 @@ export function buildProgram(): Command {
630
672
  repair: opts.repair,
631
673
  whisperModel: opts.whisperModel,
632
674
  whisperLanguage,
675
+ whisperTranslate: opts.whisperTranslate === true,
633
676
  // Split/trim/drop-empties (dictionaryFlag) — undefined stays
634
677
  // undefined so the config's dictionary can supply the default.
635
678
  dictionary: dictionaryFlag(opts.dictionary),
@@ -644,6 +687,9 @@ export function buildProgram(): Command {
644
687
  zoom: opts.zoom,
645
688
  // undefined = "not typed", so produce can let the config decide.
646
689
  watermark: opts.watermark,
690
+ // The watermark's tri-state again, resolved by resolveCoverInVideo
691
+ // at the use site against the config's `coverInVideo`.
692
+ coverInVideo: opts.coverInVideo,
647
693
  // The same tri-state contract as watermark, resolved by
648
694
  // resolveYoutube at the use site; --portrait rides along untyped =
649
695
  // undefined so the config's path can supply it.
@@ -670,6 +716,9 @@ export function buildProgram(): Command {
670
716
  // Validated by concurrencyFlag at parse time; undefined = "not
671
717
  // typed", which is what lets the config supply it.
672
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,
673
722
  });
674
723
  // Counts, buckets and names only — the duration crosses the wire as a
675
724
  // bucket, and nothing here can carry a path (assertSafeProps enforces
@@ -741,6 +790,12 @@ export function buildProgram(): Command {
741
790
  "--whisper-language <code>",
742
791
  "transcription language code for a multilingual model, e.g. ur | de | auto (whisper defaults to en)",
743
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
+ )
744
799
  .action(async (input: string, opts) => {
745
800
  const cleanup = CleanupLevelSchema.parse(opts.cleanup);
746
801
  const result = await produce(input, {
@@ -752,6 +807,7 @@ export function buildProgram(): Command {
752
807
  noiseDb: opts.noiseDb,
753
808
  whisperModel: opts.whisperModel,
754
809
  // Same guard as produce's: empty must error, not become a bare `-l`.
810
+ whisperTranslate: opts.whisperTranslate === true,
755
811
  whisperLanguage:
756
812
  opts.whisperLanguage !== undefined
757
813
  ? z.string().trim().min(1, "--whisper-language needs a code, e.g. ur").parse(opts.whisperLanguage)
@@ -991,6 +1047,68 @@ export function buildProgram(): Command {
991
1047
  });
992
1048
  });
993
1049
 
1050
+ program
1051
+ .command("publish")
1052
+ .description(
1053
+ "push the finished render to your social accounts through your own self-hosted " +
1054
+ "Postiz instance — now, or scheduled with --at",
1055
+ )
1056
+ // Optional like `edit`'s and `cover`'s: no argument resolves the run
1057
+ // under the CURRENT directory.
1058
+ .argument("[workdir]", "a work directory, or the folder you produced in")
1059
+ .option(
1060
+ "--at <iso>",
1061
+ "schedule instead of publishing now — an ISO-8601 time in the future " +
1062
+ "(e.g. 2026-09-01T08:00:00+02:00)",
1063
+ // Wrapped: commander's parseArg passes (value, previous), and atFlag's
1064
+ // second parameter is its injectable clock — not a place for `previous`.
1065
+ (v: string) => atFlag(v),
1066
+ )
1067
+ .option(
1068
+ "--platforms <list>",
1069
+ "only these platforms, comma-separated (linkedin,instagram,tiktok,x,facebook,youtube)",
1070
+ platformsFlag,
1071
+ )
1072
+ .option("--accounts <ids>", "explicit Postiz integration ids, comma-separated (the no-TTY path)", accountsFlag)
1073
+ .option("--all", "every connected account (after --platforms, when both are given)", false)
1074
+ .option("--dry-run", "print the targets and the exact payload; send nothing", false)
1075
+ .option("-y, --yes", "skip the confirmation prompt", false)
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
+ )
1089
+ .action(async (workdir: string | undefined, opts) => {
1090
+ const { runPublish } = await import("./publish");
1091
+ const target = await resolveWorkdirArgument(workdir ?? ".", "publish");
1092
+ await runPublish(target, {
1093
+ at: opts.at,
1094
+ platforms: opts.platforms,
1095
+ accounts: opts.accounts,
1096
+ all: opts.all,
1097
+ dryRun: opts.dryRun,
1098
+ yes: opts.yes,
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,
1104
+ });
1105
+ telemetry.record("publish_run", {
1106
+ scheduled: opts.at !== undefined,
1107
+ dry_run: opts.dryRun === true,
1108
+ });
1109
+ await telemetry.flush();
1110
+ });
1111
+
994
1112
  program
995
1113
  .command("setup")
996
1114
  .description(