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.
- package/package.json +4 -4
- package/src/produce.ts +41 -23
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ossclip",
|
|
3
|
-
"version": "0.1.
|
|
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.
|
|
40
|
-
"@ossclip/renderer": "0.1.
|
|
41
|
-
"@ossclip/scenes": "0.1.
|
|
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
|
-
|
|
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
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
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
|
|
2199
|
-
//
|
|
2200
|
-
//
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
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
|
-
|
|
2216
|
-
`
|
|
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:
|
|
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
|