ossclip 0.1.12 → 0.1.13

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 (2) hide show
  1. package/package.json +4 -4
  2. package/src/produce.ts +41 -23
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ossclip",
3
- "version": "0.1.12",
3
+ "version": "0.1.13",
4
4
  "description": "Local-first CLI video producer: cuts silence and fillers, word-timed captions, face-aware framing, and LLM-planned code-rendered graphics — transcription and rendering never leave your machine",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -36,9 +36,9 @@
36
36
  "commander": "^12.1.0",
37
37
  "tsx": "^4.19.0",
38
38
  "zod": "^3.25.76",
39
- "@ossclip/core": "0.1.12",
40
- "@ossclip/renderer": "0.1.12",
41
- "@ossclip/scenes": "0.1.12"
39
+ "@ossclip/core": "0.1.13",
40
+ "@ossclip/renderer": "0.1.13",
41
+ "@ossclip/scenes": "0.1.13"
42
42
  },
43
43
  "homepage": "https://github.com/AhsanAyaz/ossclip#readme",
44
44
  "bugs": {
package/src/produce.ts CHANGED
@@ -24,6 +24,7 @@ import {
24
24
  concatFolder,
25
25
  folderManifestKey,
26
26
  listFolderVideos,
27
+ coverDecision,
27
28
  coverHeadline,
28
29
  cropFilter,
29
30
  detectContentRect,
@@ -2161,13 +2162,16 @@ export async function produce(inputArg: string, opts: ProduceOptions): Promise<P
2161
2162
  // A separate file, not a burned-in intro: both platforms accept a custom
2162
2163
  // cover, so nothing has to be pickable from the video — and spending the
2163
2164
  // opening seconds on a title card fights the hook-in-2s policy directly.
2164
- if (opts.cover !== false) {
2165
+ {
2165
2166
  // §35's cap applies here too: a cached beat sheet from before the fix, or
2166
2167
  // the hook fallback, must not slip a 13-word paragraph onto a thumbnail.
2167
2168
  const coverText = coverHeadline(beatSheet?.coverText ?? beatSheet?.hook ?? "");
2168
- if (!coverText) {
2169
- console.log("▸ no cover text (run --produce for one) — skipping cover");
2170
- } else {
2169
+ // Urdu field run 2026-08-05: a run without --produce has no hook text,
2170
+ // and skipping the cover for that threw away the part that never needed
2171
+ // text — the sharpness-scored face frame. No headline now means a bare
2172
+ // frame, not no cover; see `coverDecision`'s doc comment.
2173
+ const cover = coverDecision(opts.cover !== false, coverText);
2174
+ if (cover !== "none") {
2171
2175
  const detector = await createFaceDetector();
2172
2176
  const pick = await pickCoverFrame(tools, analysisInput, analysisProbe.duration, {
2173
2177
  cacheDir: work,
@@ -2195,31 +2199,45 @@ export async function produce(inputArg: string, opts: ProduceOptions): Promise<P
2195
2199
  const coverPath = resolve(
2196
2200
  opts.coverPath ?? outPath.replace(/(\.[^.]+)?$/, ".cover.jpg"),
2197
2201
  );
2198
- // §34: if the source's own title is up at this instant, the frame
2199
- // already has a headline. Adding ours states the same claim twice in
2200
- // one image — a cover with one title beats a cover with two.
2201
- const sourceTitled = regionsDuring(
2202
- sourceText.regions,
2203
- pick.timeSec - 0.5,
2204
- pick.timeSec + 0.5,
2205
- ).length > 0;
2206
- console.log(
2207
- `▸ cover from ${pick.timeSec.toFixed(1)}s ` +
2208
- `(${pick.hasFace ? "face" : "no face"}, sharpness ${pick.sharpness.toFixed(0)})…`,
2209
- );
2210
- if (sourceTitled) {
2211
- console.log(" ▸ source already has a title in this frame — shipping it without a banner");
2212
- } else if (pick.face) {
2213
- const band = coverTextRect(pick.face, frame);
2202
+ // The §34 dedupe check and the band-placement log exist only to
2203
+ // route a banner around the frame's contents — a textless cover
2204
+ // (Urdu field run 2026-08-05) has no banner to place, so both are
2205
+ // skipped rather than run against text that isn't there.
2206
+ let bannerText = "";
2207
+ if (cover === "banner") {
2208
+ // §34: if the source's own title is up at this instant, the frame
2209
+ // already has a headline. Adding ours states the same claim twice in
2210
+ // one image — a cover with one title beats a cover with two.
2211
+ const sourceTitled = regionsDuring(
2212
+ sourceText.regions,
2213
+ pick.timeSec - 0.5,
2214
+ pick.timeSec + 0.5,
2215
+ ).length > 0;
2216
+ console.log(
2217
+ `▸ cover from ${pick.timeSec.toFixed(1)}s ` +
2218
+ `(${pick.hasFace ? "face" : "no face"}, sharpness ${pick.sharpness.toFixed(0)})…`,
2219
+ );
2220
+ if (sourceTitled) {
2221
+ console.log(" ▸ source already has a title in this frame — shipping it without a banner");
2222
+ } else if (pick.face) {
2223
+ const band = coverTextRect(pick.face, frame);
2224
+ console.log(
2225
+ ` ▸ banner in the ${band.y + band.h / 2 < pick.face.centerYFrac ? "band above" : "band below"} ` +
2226
+ `the face (${(band.y * 100).toFixed(0)}-${((band.y + band.h) * 100).toFixed(0)}%)`,
2227
+ );
2228
+ }
2229
+ bannerText = sourceTitled ? "" : coverText;
2230
+ } else {
2214
2231
  console.log(
2215
- ` ▸ banner in the ${band.y + band.h / 2 < pick.face.centerYFrac ? "band above" : "band below"} ` +
2216
- `the face (${(band.y * 100).toFixed(0)}-${((band.y + band.h) * 100).toFixed(0)}%)`,
2232
+ `▸ cover from ${pick.timeSec.toFixed(1)}s ` +
2233
+ `(${pick.hasFace ? "face" : "no face"}, sharpness ${pick.sharpness.toFixed(0)}) ` +
2234
+ `— no banner text (run --produce for one)`,
2217
2235
  );
2218
2236
  }
2219
2237
  await renderCover(
2220
2238
  {
2221
2239
  frameFileName: frameName,
2222
- text: sourceTitled ? "" : coverText,
2240
+ text: bannerText,
2223
2241
  theme,
2224
2242
  face: pick.face,
2225
2243
  // The cover is the OUTPUT's thumbnail — a landscape render gets a