@ossclip/core 0.1.27 → 0.1.29

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ossclip/core",
3
- "version": "0.1.27",
3
+ "version": "0.1.29",
4
4
  "description": "ossclip's framework-free pipeline: schema, transcription, analysis, cutlist, captions, framing, and the LLM producer",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/src/browser.ts CHANGED
@@ -45,11 +45,72 @@ export {
45
45
  // types only). The editor's load-path repair needs the MAP, not the raw span
46
46
  // array, to decide whether a repair is possible at all — see
47
47
  // `anchorCaptionLines` (§137): a non-empty array can still build an empty map.
48
- export { mapFromKeptSpans, TimeMap, type KeptSpan } from "./timemap";
48
+ // `mapsClose` rides along since cut review step 4: the editor's playhead
49
+ // hand-off across a live re-cut needs "did the clock actually change" to be
50
+ // the SAME float-tolerant comparison `livePreviewMap`'s identity gate uses,
51
+ // or a 1-ulp drift could seek the player for nothing.
52
+ export { mapFromKeptSpans, mapsClose, TimeMap, type KeptSpan } from "./timemap";
49
53
  // The §35 cover word cap. The editor's CoverPanel shows the trimmed headline
50
54
  // live as you type, and restating the trimming rules there would drift from
51
55
  // the one the regenerate endpoint actually renders with. Imported from
52
56
  // ./cover-headline, NOT ./cover — that module is node all the way down
53
57
  // (node:fs, ./exec), which is exactly what this surface exists to keep out.
54
58
  export { COVER_MAX_WORDS, coverHeadline } from "./cover-headline";
55
- export type { Probe, Production, RenderSettings, Segment, Transcript, Word } from "./schema";
59
+ // The cleanup veto layer (cut review step 3), VALUE exports and browser-safe:
60
+ // cutlist.ts imports nothing but types from ./schema — verified before this
61
+ // export, zero node built-ins in its graph. The editor marks vetoed seams
62
+ // with the SAME `applyCleanupChoices` produce renders with (the
63
+ // buildCoverRender one-implementation-two-callers pattern); a browser copy is
64
+ // how the preview and the render would drift. `buildCutlist` itself rides
65
+ // along in the module graph but stays unexported here on purpose — the
66
+ // editor must never rebuild the proposal, only apply choices to the one
67
+ // produce recorded.
68
+ export {
69
+ applyCleanupChoices,
70
+ cleanupVetoable,
71
+ vetoedRemovals,
72
+ type CleanupChoices,
73
+ } from "./cutlist";
74
+ // The live post-veto preview (cut review step 4), VALUE exports and
75
+ // browser-safe: retime-preview.ts composes cutlist + recut + timemap — all
76
+ // already in this surface's runtime graph (recut.ts imports only overrides
77
+ // and timemap, zero node built-ins, verified before this export). The editor
78
+ // re-cuts its preview clock with the SAME `applyCleanupChoices` +
79
+ // `subtractRangesFromCutlist` sequence produce runs and re-times every prop
80
+ // through the SAME `remapPoint` produce re-anchors with — one implementation,
81
+ // two callers, so the preview cannot drift from the render.
82
+ // `previewClockMappers` rides along (step 4 follow-up): the surfaces that
83
+ // speak in single instants — transcript seeks, ghost bands, the cover
84
+ // panel's playhead — need the same walk as a point function, identity when
85
+ // no re-cut is live, threaded by App so no consumer learns the machinery.
86
+ // `cutRangeToOldClock` is the WRITE direction's range half (the follow-up's
87
+ // follow-up): a cut gesture's live window converted to the old-clock frame
88
+ // `doc.cuts` speaks, shared by the Inspector's button and App's delete
89
+ // modal so the shrink/refuse verdict cannot drift between the two.
90
+ export {
91
+ cutRangeToOldClock,
92
+ livePreviewMap,
93
+ previewClockMappers,
94
+ retimeForPreview,
95
+ type LivePreviewClocks,
96
+ type OldClockCutRange,
97
+ type PreviewClockMappers,
98
+ type RetimeablePreviewProps,
99
+ type RetimedPreviewFields,
100
+ } from "./retime-preview";
101
+ export type {
102
+ Probe,
103
+ Production,
104
+ // `RemovalReason` rides along with `Segment` (cut review step 2): the
105
+ // editor's reason→colour map is a `Record<RemovalReason, string>` precisely
106
+ // so a NEW reason in the vocabulary fails typecheck in the editor instead
107
+ // of silently drawing an uncoloured seam. (Since step 3 the schema module
108
+ // is in the runtime graph anyway — ./overrides imports RemovalReasonSchema
109
+ // for the `cleanup` key — but schema.ts is zod + scene-schema only, both
110
+ // already on this surface, so it stays browser-safe.)
111
+ RemovalReason,
112
+ RenderSettings,
113
+ Segment,
114
+ Transcript,
115
+ Word,
116
+ } from "./schema";
package/src/config.ts CHANGED
@@ -19,6 +19,16 @@ export interface OssclipConfig {
19
19
  * sheet always uses the main model. "same" disables tiering (FINDINGS §37).
20
20
  */
21
21
  fastModel?: string;
22
+ /**
23
+ * Reasoning effort for the antigravity provider — low | medium | high,
24
+ * agy's own `--effort` vocabulary. Consumed ONLY by antigravity today
25
+ * (§143: exposed after the hang incident — the knob existed and we passed
26
+ * nothing); every other provider ignores it. `--llm-effort` wins over this
27
+ * per run. File-only like `dictionary`; validated at the consumer
28
+ * (`resolveLlmEffort` in produce.ts), so a hand-edited `"max"` is one
29
+ * warning and an ignored key, never a coerced effort level.
30
+ */
31
+ llmEffort?: string;
22
32
  /**
23
33
  * Download URLs for models the ggerganov mirror doesn't host, keyed by the
24
34
  * bare model name — a user's own fine-tune needs one line:
@@ -196,6 +206,11 @@ export function loadConfig(): OssclipConfig {
196
206
  modelDir: process.env.OSSCLIP_MODEL_DIR ?? fileCfg.modelDir ?? DEFAULTS.modelDir,
197
207
  model: process.env.OSSCLIP_MODEL ?? fileCfg.model ?? DEFAULTS.model,
198
208
  fastModel: process.env.OSSCLIP_FAST_MODEL ?? fileCfg.fastModel,
209
+ // File-only, the `dictionary` posture — and deliberately NO env spelling
210
+ // (flag + config are the whole interface): validated where it is USED
211
+ // (`resolveLlmEffort` in produce.ts), so a hand-edited `"max"` earns one
212
+ // warning there and agy's default, never a coerced effort.
213
+ llmEffort: fileCfg.llmEffort,
199
214
  speaker: process.env.OSSCLIP_SPEAKER ?? fileCfg.speaker,
200
215
  openEditorAfterProduce: (process.env.OSSCLIP_OPEN_EDITOR ??
201
216
  fileCfg.openEditorAfterProduce) as OpenEditorPref | undefined,
@@ -26,10 +26,17 @@
26
26
  */
27
27
  export const COVER_MAX_WORDS = 9;
28
28
 
29
- /** Trailing words that cannot end a headline — the truncation reads as broken. */
29
+ /**
30
+ * Trailing words that cannot end a headline — the truncation reads as broken.
31
+ * Auxiliaries dangle exactly like prepositions: a real run (2026-08-22)
32
+ * truncated to "AI Gave Me Too Many Ideas. I Had" because the set had none.
33
+ * "i" is here for the same incident: popping "Had" alone leaves "…Ideas. I",
34
+ * a subject with its sentence cut off.
35
+ */
30
36
  const DANGLING = new Set([
31
37
  "a", "an", "and", "as", "at", "but", "by", "for", "from", "in", "is", "it",
32
38
  "of", "on", "or", "the", "to", "with", "that", "this", "my", "your", "so",
39
+ "had", "has", "have", "was", "were", "will", "can", "should", "i",
33
40
  ]);
34
41
 
35
42
  /**
package/src/cutlist.ts CHANGED
@@ -329,3 +329,112 @@ export function buildCutlist({
329
329
 
330
330
  return segments;
331
331
  }
332
+
333
+ /**
334
+ * The user's veto layer over the automatic cutlist (cut review step 3):
335
+ * `buildCutlist` PROPOSES removals, this is how the user DECLINES some of
336
+ * them — per category ("keep all pauses") and per individual span. Persisted
337
+ * as `overrides.json`'s `cleanup` key (`OverrideDocSchema.cleanup`, which
338
+ * owns the on-disk contract); this structural interface is what the pure
339
+ * functions below accept, so this module stays free of zod and of the
340
+ * overrides module — callable from produce AND from the editor bundle with
341
+ * nothing but the schema's own types.
342
+ */
343
+ export interface CleanupChoices {
344
+ /**
345
+ * Category master switches. `false` = do not remove this reason's spans;
346
+ * absent (or a tolerated `true` on disk) = the default, remove as proposed.
347
+ */
348
+ reasons?: Partial<Record<RemovalReason, boolean>>;
349
+ /** Individual vetoes, SOURCE seconds — see `vetoedRemovals` for the
350
+ * overlap-based matching rule. */
351
+ kept?: readonly { srcIn: number; srcOut: number }[];
352
+ }
353
+
354
+ /**
355
+ * Whether a removal reason CAN be declined at all. `user` cuts come from the
356
+ * `cuts[]` array — declining your own cut is Restore on the cut, an existing
357
+ * gesture, and a second way to undo it would leave the entry behind as dead
358
+ * weight (there is no "not cut" state for a `cuts` entry to hold, per its
359
+ * schema comment). `clip` is the `--clip` window, a selection decision, not a
360
+ * cleanup — "keeping" a clip removal would silently un-clip the video. One
361
+ * predicate, exported, so produce, the panel's checkboxes and the timeline's
362
+ * seam handler cannot disagree about what is toggleable.
363
+ */
364
+ export function cleanupVetoable(reason: RemovalReason | undefined): boolean {
365
+ return reason !== "user" && reason !== "clip";
366
+ }
367
+
368
+ /**
369
+ * The `remove` spans of `cutlist` that `choices` declines — the shared
370
+ * predicate under `applyCleanupChoices` (which re-keeps exactly these) and
371
+ * the editor's vetoed-seam state / produce's "kept N pause removal(s)" line
372
+ * (which both NAME them). One list, computed once, so what the seam shows as
373
+ * "will be kept" and what the render actually keeps cannot drift.
374
+ *
375
+ * Individual vetoes match by OVERLAP, deliberately NOT by float equality of
376
+ * endpoints: a re-produce can shift a removal's boundary by a frame (a
377
+ * changed silence threshold, a repair that re-stamps a word), and an
378
+ * exact-match veto that silently stops matching is the failure mode — the
379
+ * pause the user declined would quietly start being cut again. A partial
380
+ * overlap re-keeps the WHOLE removal span: a removal is one decision, not
381
+ * divisible.
382
+ */
383
+ export function vetoedRemovals(
384
+ cutlist: readonly Segment[],
385
+ choices: CleanupChoices | undefined,
386
+ ): Segment[] {
387
+ if (!choices) return [];
388
+ const kept = choices.kept ?? [];
389
+ return cutlist.filter(
390
+ (seg) =>
391
+ seg.kind === "remove" &&
392
+ cleanupVetoable(seg.reason) &&
393
+ ((seg.reason !== undefined && choices.reasons?.[seg.reason] === false) ||
394
+ kept.some((k) => k.srcIn < seg.srcOut && k.srcOut > seg.srcIn)),
395
+ );
396
+ }
397
+
398
+ /**
399
+ * Apply the user's cleanup choices to the automatic cutlist: every vetoed
400
+ * removal (see `vetoedRemovals`) becomes a keep again, and adjacent keeps
401
+ * merge so the result stays the same canonical shape `buildCutlist` emits —
402
+ * a full partition of the input's range with no gaps, no overlaps, monotonic
403
+ * (the invariants `TimeMap`'s constructor checks). The partition holds BY
404
+ * CONSTRUCTION: spans are only relabelled and merged, never moved, so the
405
+ * covered range cannot change.
406
+ *
407
+ * `undefined`/empty choices return the input content unchanged — the
408
+ * regression anchor: an overrides.json with no `cleanup` key must produce a
409
+ * byte-identical render.
410
+ *
411
+ * ONE implementation, two callers (the `buildCoverRender` pattern): produce
412
+ * calls this between `buildCutlist` and the `TimeMap`; the editor calls it
413
+ * (via `@ossclip/core/browser`) to mark vetoed seams. A preview that
414
+ * disagrees with the render is worse than no preview.
415
+ */
416
+ export function applyCleanupChoices(
417
+ cutlist: readonly Segment[],
418
+ choices: CleanupChoices | undefined,
419
+ ): Segment[] {
420
+ const vetoed = new Set(vetoedRemovals(cutlist, choices));
421
+ if (vetoed.size === 0) return [...cutlist];
422
+ const out: Segment[] = [];
423
+ for (const seg of cutlist) {
424
+ // A re-kept span drops `reason`/`confidence` — they described a removal
425
+ // that is no longer happening, and a keep carrying "pause" would read as
426
+ // a seventh segment kind everywhere the partition is consumed.
427
+ const next: Segment = vetoed.has(seg)
428
+ ? { srcIn: seg.srcIn, srcOut: seg.srcOut, kind: "keep" }
429
+ : seg;
430
+ const prev = out[out.length - 1];
431
+ if (prev !== undefined && prev.kind === "keep" && next.kind === "keep") {
432
+ // Merge in place — `prev` is always this function's own object (pushed
433
+ // below as a fresh literal when it is a keep), never the caller's.
434
+ prev.srcOut = next.srcOut;
435
+ continue;
436
+ }
437
+ out.push(next.kind === "keep" ? { srcIn: next.srcIn, srcOut: next.srcOut, kind: "keep" } : next);
438
+ }
439
+ return out;
440
+ }
package/src/index.ts CHANGED
@@ -12,6 +12,7 @@ export * from "./concat";
12
12
  export * from "./transcribe";
13
13
  export * from "./analyze";
14
14
  export * from "./cutlist";
15
+ export * from "./retime-preview";
15
16
  export * from "./clip";
16
17
  export * from "./blooper";
17
18
  export * from "./retake";
package/src/overrides.ts CHANGED
@@ -7,6 +7,7 @@ import {
7
7
  type SceneComponentId,
8
8
  type Theme,
9
9
  } from "./scene-schema";
10
+ import { RemovalReasonSchema } from "./schema";
10
11
  import { resolveSceneProps } from "./scene-registry";
11
12
  import type { CaptionLine, CaptionWord } from "./captions";
12
13
 
@@ -435,6 +436,42 @@ export const OverrideDocSchema = z.object({
435
436
  }),
436
437
  )
437
438
  .default([]),
439
+ /**
440
+ * The user's VETO over the automatic cutlist (cut review step 3). `cuts`
441
+ * above is the user ADDING a removal; this is the user DECLINING one the
442
+ * pipeline proposed — opposite directions, deliberately not merged.
443
+ * Consumed by `applyCleanupChoices` (cutlist.ts, which owns the matching
444
+ * semantics), in produce and in the editor alike.
445
+ *
446
+ * `reasons` are the category master switches ("keep all pauses"). Only
447
+ * `false` is ever WRITTEN — a `true` entry restates the default, and the
448
+ * editor DELETES the key instead (the `hidden`/`captionsHidden` rule: an
449
+ * override with nothing to say). A `true` on disk is still parsed and
450
+ * means default, tolerantly. `user` and `clip` keys parse but are inert
451
+ * (`cleanupVetoable`): declining your own cut is Restore on the cut, and
452
+ * "keeping" the --clip window's removal would silently un-clip the video.
453
+ *
454
+ * `kept` are individual vetoes, in SOURCE seconds — and that anchoring is
455
+ * the whole trick, same as `cuts[].src` above: a bare output-seconds pair
456
+ * is meaningless once its own re-cut has happened, while source time is
457
+ * stable across every re-cut. This layer is therefore RECUT-IMMUNE BY
458
+ * CONSTRUCTION and — unlike `splits` and `scenes[*].timing` — needs NO
459
+ * entry in `remapOverridesThroughRecut`. Matching against the (possibly
460
+ * re-produced) cutlist is by OVERLAP, never float equality of endpoints;
461
+ * `vetoedRemovals` (cutlist.ts) states why.
462
+ *
463
+ * Optional-with-default like `splits`/`cuts`, so every overrides.json
464
+ * written before the key existed parses byte-identically; absent means
465
+ * today's behaviour exactly.
466
+ */
467
+ cleanup: z
468
+ .object({
469
+ reasons: z.partialRecord(RemovalReasonSchema, z.boolean()).default({}),
470
+ kept: z
471
+ .array(z.object({ srcIn: z.number().nonnegative(), srcOut: z.number().nonnegative() }))
472
+ .default([]),
473
+ })
474
+ .default({ reasons: {}, kept: [] }),
438
475
  });
439
476
  export type OverrideDoc = z.infer<typeof OverrideDocSchema>;
440
477