dreamcontext 0.6.0 → 0.7.0
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/README.md +2 -1
- package/agents/sleep-product.md +19 -2
- package/agents/sleep-state.md +43 -22
- package/agents/sleep-tasks.md +18 -0
- package/dist/agents/sleep-product.md +19 -2
- package/dist/agents/sleep-state.md +43 -22
- package/dist/agents/sleep-tasks.md +18 -0
- package/dist/dashboard/assets/{BrainCanvas3D-LLqVeXtb.js → BrainCanvas3D-lFgJbbhZ.js} +1 -1
- package/dist/dashboard/assets/{_baseUniq-DW0uA0ty.js → _baseUniq-BpANgc_i.js} +1 -1
- package/dist/dashboard/assets/{arc-fVUGtlbL.js → arc-CX32Jm7E.js} +1 -1
- package/dist/dashboard/assets/{architectureDiagram-Q4EWVU46-Dz2PKBsS.js → architectureDiagram-Q4EWVU46-ARASlGxO.js} +1 -1
- package/dist/dashboard/assets/{blockDiagram-DXYQGD6D-ByWKxzhL.js → blockDiagram-DXYQGD6D-BBvsYm9E.js} +1 -1
- package/dist/dashboard/assets/{c4Diagram-AHTNJAMY-dpHsVM3D.js → c4Diagram-AHTNJAMY-ChSUfXR9.js} +1 -1
- package/dist/dashboard/assets/channel-w_Bp182N.js +1 -0
- package/dist/dashboard/assets/{chunk-4BX2VUAB-HEXb6Yg5.js → chunk-4BX2VUAB-COHoVEpt.js} +1 -1
- package/dist/dashboard/assets/{chunk-4TB4RGXK-DeVy5g6H.js → chunk-4TB4RGXK-DKJFLaTC.js} +1 -1
- package/dist/dashboard/assets/{chunk-55IACEB6-DGy3ZgDZ.js → chunk-55IACEB6-CKZPWZRu.js} +1 -1
- package/dist/dashboard/assets/{chunk-EDXVE4YY-CYJehjz4.js → chunk-EDXVE4YY-k1y4mGUJ.js} +1 -1
- package/dist/dashboard/assets/{chunk-FMBD7UC4-DKTOmvgZ.js → chunk-FMBD7UC4-ixXe5R10.js} +1 -1
- package/dist/dashboard/assets/{chunk-OYMX7WX6-mmtWyiDA.js → chunk-OYMX7WX6-NDFap7xg.js} +1 -1
- package/dist/dashboard/assets/{chunk-QZHKN3VN-DSdV0iwY.js → chunk-QZHKN3VN-vY0UpHjB.js} +1 -1
- package/dist/dashboard/assets/{chunk-YZCP3GAM-DbY-KoDn.js → chunk-YZCP3GAM-yztvsKR-.js} +1 -1
- package/dist/dashboard/assets/classDiagram-6PBFFD2Q-CcQxcqy9.js +1 -0
- package/dist/dashboard/assets/classDiagram-v2-HSJHXN6E-CcQxcqy9.js +1 -0
- package/dist/dashboard/assets/clone-_znoR_ci.js +1 -0
- package/dist/dashboard/assets/{cose-bilkent-S5V4N54A-p0t1DY88.js → cose-bilkent-S5V4N54A-DKXM5Fh5.js} +1 -1
- package/dist/dashboard/assets/{dagre-KV5264BT-DYE0WzHM.js → dagre-KV5264BT-DN1Nlsmy.js} +1 -1
- package/dist/dashboard/assets/{diagram-5BDNPKRD-D9EiQCOP.js → diagram-5BDNPKRD-DnoJFRqR.js} +1 -1
- package/dist/dashboard/assets/{diagram-G4DWMVQ6-poDXSAfu.js → diagram-G4DWMVQ6-CF1_jTNI.js} +1 -1
- package/dist/dashboard/assets/{diagram-MMDJMWI5-CHZ7wgN1.js → diagram-MMDJMWI5-D4-bZZEt.js} +1 -1
- package/dist/dashboard/assets/{diagram-TYMM5635-DVk6fYG7.js → diagram-TYMM5635-al8RqVgf.js} +1 -1
- package/dist/dashboard/assets/{erDiagram-SMLLAGMA-D2sqkGin.js → erDiagram-SMLLAGMA-MEcC0rwO.js} +1 -1
- package/dist/dashboard/assets/{flowDiagram-DWJPFMVM-DrMlKBYA.js → flowDiagram-DWJPFMVM-DCLyNpW8.js} +1 -1
- package/dist/dashboard/assets/{ganttDiagram-T4ZO3ILL-Cf4FbEFp.js → ganttDiagram-T4ZO3ILL-DfEvbbJK.js} +1 -1
- package/dist/dashboard/assets/{gitGraphDiagram-UUTBAWPF-BsbOq1C9.js → gitGraphDiagram-UUTBAWPF-C_YozdVL.js} +1 -1
- package/dist/dashboard/assets/{graph-CUxxgCtS.js → graph-DpIXS1G1.js} +1 -1
- package/dist/dashboard/assets/index-Bo5CUa_M.js +480 -0
- package/dist/dashboard/assets/index-DrqurW1c.css +1 -0
- package/dist/dashboard/assets/{infoDiagram-42DDH7IO-DTkMnZiD.js → infoDiagram-42DDH7IO-AdT6kjzj.js} +1 -1
- package/dist/dashboard/assets/{ishikawaDiagram-UXIWVN3A-CahQ348K.js → ishikawaDiagram-UXIWVN3A-B0_9IZVO.js} +1 -1
- package/dist/dashboard/assets/{journeyDiagram-VCZTEJTY-BDY2Nslc.js → journeyDiagram-VCZTEJTY-BveNBswQ.js} +1 -1
- package/dist/dashboard/assets/{kanban-definition-6JOO6SKY-CgrwoTjI.js → kanban-definition-6JOO6SKY-CFI8j4jR.js} +1 -1
- package/dist/dashboard/assets/{layout-IaUxkFkm.js → layout-DI7XjZy1.js} +1 -1
- package/dist/dashboard/assets/{linear-shc0iNFn.js → linear-CSjp56iw.js} +1 -1
- package/dist/dashboard/assets/{min-BIL7YgTN.js → min-pAGUJmEC.js} +1 -1
- package/dist/dashboard/assets/{mindmap-definition-QFDTVHPH-GmO0JRtA.js → mindmap-definition-QFDTVHPH-C2mBnknr.js} +1 -1
- package/dist/dashboard/assets/{pieDiagram-DEJITSTG-Bfm_toYA.js → pieDiagram-DEJITSTG-i_phWDqD.js} +1 -1
- package/dist/dashboard/assets/{quadrantDiagram-34T5L4WZ-DIjTM3lm.js → quadrantDiagram-34T5L4WZ-Dv7TGJjw.js} +1 -1
- package/dist/dashboard/assets/{requirementDiagram-MS252O5E-B9AEoGYh.js → requirementDiagram-MS252O5E-CB2Jl-O5.js} +1 -1
- package/dist/dashboard/assets/{sankeyDiagram-XADWPNL6-C_O5XLXm.js → sankeyDiagram-XADWPNL6-DxCoN-EI.js} +1 -1
- package/dist/dashboard/assets/{sequenceDiagram-FGHM5R23-CHSxSvmJ.js → sequenceDiagram-FGHM5R23-BeZyaehJ.js} +1 -1
- package/dist/dashboard/assets/{stateDiagram-FHFEXIEX-B0URrhHw.js → stateDiagram-FHFEXIEX-D3AjQzD1.js} +1 -1
- package/dist/dashboard/assets/stateDiagram-v2-QKLJ7IA2-Ci9v--xK.js +1 -0
- package/dist/dashboard/assets/{timeline-definition-GMOUNBTQ-CycU2gVC.js → timeline-definition-GMOUNBTQ-3l_8RFUg.js} +1 -1
- package/dist/dashboard/assets/{vennDiagram-DHZGUBPP-DcU2536G.js → vennDiagram-DHZGUBPP-2QiY25JD.js} +1 -1
- package/dist/dashboard/assets/{wardley-RL74JXVD-BWqqaX3S.js → wardley-RL74JXVD-DNLmFofz.js} +1 -1
- package/dist/dashboard/assets/{wardleyDiagram-NUSXRM2D-CUnIHJgd.js → wardleyDiagram-NUSXRM2D-BI0tupiS.js} +1 -1
- package/dist/dashboard/assets/{xychartDiagram-5P7HB3ND-b9fcOYTj.js → xychartDiagram-5P7HB3ND-CghXPE7_.js} +1 -1
- package/dist/dashboard/index.html +2 -2
- package/dist/index.js +1669 -1064
- package/dist/skill-packs/excalidraw/SKILL.md +82 -3
- package/dist/skill-packs/video-watching/SKILL.md +54 -13
- package/dist/skill-packs/video-watching/scripts/build_frame_index.py +61 -10
- package/dist/skill-packs/video-watching/scripts/transcribe.sh +147 -54
- package/dist/templates/init/data-structures/default.md +26 -24
- package/package.json +1 -1
- package/skill/SKILL.md +26 -12
- package/skill-packs/excalidraw/SKILL.md +82 -3
- package/skill-packs/video-watching/SKILL.md +54 -13
- package/skill-packs/video-watching/scripts/build_frame_index.py +61 -10
- package/skill-packs/video-watching/scripts/transcribe.sh +147 -54
- package/dist/dashboard/assets/channel-zrLwggBX.js +0 -1
- package/dist/dashboard/assets/classDiagram-6PBFFD2Q-BvejNiwH.js +0 -1
- package/dist/dashboard/assets/classDiagram-v2-HSJHXN6E-BvejNiwH.js +0 -1
- package/dist/dashboard/assets/clone-BFVdml6g.js +0 -1
- package/dist/dashboard/assets/index-CqSkXBSu.css +0 -1
- package/dist/dashboard/assets/index-flRpQtDj.js +0 -476
- package/dist/dashboard/assets/stateDiagram-v2-QKLJ7IA2-FdAb9MBo.js +0 -1
- package/dist/skill-packs/video-watching/scripts/gap_fill.py +0 -45
- package/skill-packs/video-watching/scripts/gap_fill.py +0 -45
|
@@ -6,30 +6,92 @@
|
|
|
6
6
|
# video. Claude reads the outputs (transcript + frames.json), decides which frames
|
|
7
7
|
# matter, views them, and writes the curated <video>.transcript.md (see SKILL.md).
|
|
8
8
|
#
|
|
9
|
-
# Usage: ./transcribe.sh "/abs/path/to/video.mov" [lang]
|
|
9
|
+
# Usage: ./transcribe.sh "/abs/path/to/video.mov" [lang] [flags]
|
|
10
10
|
# lang: whisper language code. DEFAULT 'auto' — whisper detects it, no need to pass.
|
|
11
11
|
# Force only if auto mislabels a short/ambiguous clip (e.g. 'tr', 'en').
|
|
12
12
|
#
|
|
13
|
+
# Flags (also settable as env vars):
|
|
14
|
+
# --frames-only skip whisper entirely; extract frames only. For pure
|
|
15
|
+
# UI/UX teardowns where no transcript is needed. (FRAMES_ONLY=1)
|
|
16
|
+
# --mode ui|lecture sampling preset. 'ui' = MAX_GAP 2.5s (app screens linger
|
|
17
|
+
# 3-5s and change by text only, below scene-detect). 'lecture'
|
|
18
|
+
# (default) = MAX_GAP 10s for talking-head video. (MODE=ui)
|
|
19
|
+
# --max-gap N guarantee a frame at least every N seconds. Overrides the
|
|
20
|
+
# mode preset. (MAX_GAP=N)
|
|
21
|
+
# --scene-threshold N ffmpeg scene-change sensitivity, lower = more frames. (SCENE_THRESHOLD=N)
|
|
22
|
+
# --contact-sheet also emit frames/contact_sheet.jpg — a tiled montage of every
|
|
23
|
+
# frame, so coverage is verifiable in one glance. (CONTACT_SHEET=1)
|
|
24
|
+
# --lang CODE same as the positional lang arg.
|
|
25
|
+
#
|
|
13
26
|
# Env overrides:
|
|
14
27
|
# WHISPER_MODEL=/abs/path/ggml-*.bin force a specific model
|
|
15
28
|
# OUT_DIR=/abs/path where artifacts go (default: <video_dir>/<slug>.media)
|
|
16
|
-
#
|
|
17
|
-
#
|
|
29
|
+
# NOTE: avoid ':' in OUT_DIR — it is special inside the
|
|
30
|
+
# ffmpeg filter graph and would truncate the metadata path.
|
|
31
|
+
# DEDUPE_SEC=0.4 collapse non-anchor frames closer than this
|
|
18
32
|
#
|
|
19
33
|
# Outputs (in OUT_DIR):
|
|
20
|
-
# transcript.srt timestamped segments (human-friendly)
|
|
21
|
-
# transcript.json/.txt/.vtt
|
|
22
|
-
# frames/
|
|
23
|
-
# frames/gap_*.jpg fill frames where scene-detect left a stretch > MAX_GAP unsampled
|
|
34
|
+
# transcript.srt timestamped segments (human-friendly) [skipped with --frames-only]
|
|
35
|
+
# transcript.json/.txt/.vtt [skipped with --frames-only]
|
|
36
|
+
# frames/frame_*.jpg selected frames (scene change OR every MAX_GAP, whichever first)
|
|
24
37
|
# frames/anchor_first.jpg / anchor_last.jpg always-captured first + last frame
|
|
38
|
+
# frames/contact_sheet.jpg tiled montage of all frames [--contact-sheet only]
|
|
25
39
|
# frames.json [{file, t, at, type}] — each frame's pts_time, sorted (Claude reads this)
|
|
26
40
|
# source-meta.txt ffprobe dump (audio.wav is created then deleted after transcription)
|
|
27
41
|
set -euo pipefail
|
|
28
42
|
|
|
29
|
-
|
|
30
|
-
|
|
43
|
+
# --- arg + flag parsing ---------------------------------------------------------
|
|
44
|
+
# Positional: first non-flag = VIDEO, second non-flag = LANG_CODE (back-compatible).
|
|
45
|
+
VIDEO=""
|
|
46
|
+
LANG_CODE=""
|
|
47
|
+
FRAMES_ONLY="${FRAMES_ONLY:-0}"
|
|
48
|
+
MODE="${MODE:-lecture}"
|
|
49
|
+
CONTACT_SHEET="${CONTACT_SHEET:-0}"
|
|
50
|
+
# MAX_GAP / SCENE_THRESHOLD: track whether explicitly set so a CLI/env value beats the
|
|
51
|
+
# mode preset. Empty here means "fall back to the mode preset" computed below.
|
|
52
|
+
MAX_GAP="${MAX_GAP:-}"
|
|
53
|
+
SCENE_THRESHOLD="${SCENE_THRESHOLD:-}"
|
|
54
|
+
|
|
55
|
+
while [ $# -gt 0 ]; do
|
|
56
|
+
case "$1" in
|
|
57
|
+
--frames-only) FRAMES_ONLY=1 ;;
|
|
58
|
+
--contact-sheet) CONTACT_SHEET=1 ;;
|
|
59
|
+
--mode) MODE="${2:?--mode needs a value}"; shift ;;
|
|
60
|
+
--mode=*) MODE="${1#*=}" ;;
|
|
61
|
+
--max-gap) MAX_GAP="${2:?--max-gap needs a value}"; shift ;;
|
|
62
|
+
--max-gap=*) MAX_GAP="${1#*=}" ;;
|
|
63
|
+
--scene-threshold) SCENE_THRESHOLD="${2:?--scene-threshold needs a value}"; shift ;;
|
|
64
|
+
--scene-threshold=*) SCENE_THRESHOLD="${1#*=}" ;;
|
|
65
|
+
--lang) LANG_CODE="${2:?--lang needs a value}"; shift ;;
|
|
66
|
+
--lang=*) LANG_CODE="${1#*=}" ;;
|
|
67
|
+
--*) echo "unknown flag: $1" >&2; exit 2 ;;
|
|
68
|
+
*) if [ -z "$VIDEO" ]; then VIDEO="$1"
|
|
69
|
+
elif [ -z "$LANG_CODE" ]; then LANG_CODE="$1"
|
|
70
|
+
else echo "unexpected arg: $1" >&2; exit 2; fi ;;
|
|
71
|
+
esac
|
|
72
|
+
shift
|
|
73
|
+
done
|
|
74
|
+
|
|
75
|
+
[ -n "$VIDEO" ] || { echo "Usage: transcribe.sh <video> [lang] [--frames-only] [--mode ui] [--max-gap N]" >&2; exit 1; }
|
|
76
|
+
LANG_CODE="${LANG_CODE:-auto}" # whisper auto-detects the spoken language unless overridden
|
|
77
|
+
|
|
78
|
+
# Mode presets fill in only what the caller left unset (explicit CLI/env always wins).
|
|
79
|
+
case "$MODE" in
|
|
80
|
+
ui) MODE_MAX_GAP=2.5 ;;
|
|
81
|
+
lecture) MODE_MAX_GAP=10 ;;
|
|
82
|
+
*) echo "unknown --mode '$MODE' (use ui|lecture)" >&2; exit 2 ;;
|
|
83
|
+
esac
|
|
84
|
+
MAX_GAP="${MAX_GAP:-$MODE_MAX_GAP}"
|
|
31
85
|
SCENE_THRESHOLD="${SCENE_THRESHOLD:-0.15}"
|
|
32
|
-
|
|
86
|
+
|
|
87
|
+
# Guard the two numeric knobs before they reach the ffmpeg filter / Python: a non-number
|
|
88
|
+
# would crash build_frame_index (float()), and MAX_GAP=0 makes gte(t-prev,0) select EVERY
|
|
89
|
+
# frame (disk-fill footgun). Fail loudly instead.
|
|
90
|
+
is_num='^[0-9]+([.][0-9]+)?$'
|
|
91
|
+
[[ "$MAX_GAP" =~ $is_num ]] || { echo "--max-gap must be a number (got '$MAX_GAP')" >&2; exit 2; }
|
|
92
|
+
awk "BEGIN{exit !($MAX_GAP > 0)}" || { echo "--max-gap must be > 0 (got '$MAX_GAP')" >&2; exit 2; }
|
|
93
|
+
[[ "$SCENE_THRESHOLD" =~ $is_num ]] || { echo "--scene-threshold must be a number (got '$SCENE_THRESHOLD')" >&2; exit 2; }
|
|
94
|
+
|
|
33
95
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
34
96
|
|
|
35
97
|
# --- remote link? download with yt-dlp (local files skip this) ------------------
|
|
@@ -43,7 +105,10 @@ if [[ "$VIDEO" =~ ^https?:// ]]; then
|
|
|
43
105
|
fi
|
|
44
106
|
[ -f "$VIDEO" ] || { echo "video not found: $VIDEO" >&2; exit 1; }
|
|
45
107
|
|
|
108
|
+
command -v ffmpeg >/dev/null || { echo "ffmpeg not found (brew install ffmpeg)" >&2; exit 1; }
|
|
109
|
+
|
|
46
110
|
# --- model: prefer turbo, then large-v3, then medium (override with WHISPER_MODEL)
|
|
111
|
+
# Only needed for transcription — skip the whole resolution when --frames-only.
|
|
47
112
|
pick_model() {
|
|
48
113
|
if [ -n "${WHISPER_MODEL:-}" ]; then printf '%s' "$WHISPER_MODEL"; return; fi
|
|
49
114
|
local p
|
|
@@ -57,18 +122,19 @@ pick_model() {
|
|
|
57
122
|
done
|
|
58
123
|
printf ''
|
|
59
124
|
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
125
|
+
if [ "$FRAMES_ONLY" != "1" ]; then
|
|
126
|
+
MODEL="$(pick_model)"
|
|
127
|
+
[ -n "$MODEL" ] && [ -f "$MODEL" ] || {
|
|
128
|
+
echo "no whisper model found. Install one, e.g.:" >&2
|
|
129
|
+
echo " curl -L -o ~/.cache/whisper.cpp/models/ggml-large-v3-turbo.bin \\" >&2
|
|
130
|
+
echo " https://huggingface.co/ggerganov/whisper.cpp/resolve/main/ggml-large-v3-turbo.bin" >&2
|
|
131
|
+
echo " or set WHISPER_MODEL=/abs/path/ggml-*.bin" >&2
|
|
132
|
+
echo " (or pass --frames-only to skip transcription entirely)" >&2
|
|
133
|
+
exit 1
|
|
134
|
+
}
|
|
135
|
+
WHISPER_BIN="$(command -v whisper-cli || command -v whisper-cpp || command -v main || true)"
|
|
136
|
+
[ -n "$WHISPER_BIN" ] || { echo "whisper.cpp binary not found (brew install whisper-cpp). Or pass --frames-only." >&2; exit 1; }
|
|
137
|
+
fi
|
|
72
138
|
|
|
73
139
|
# --- where artifacts go: next to the video by default ---------------------------
|
|
74
140
|
SRC_DIR="$(cd "$(dirname "$VIDEO")" && pwd)"
|
|
@@ -79,28 +145,51 @@ mkdir -p "$OUT/frames"
|
|
|
79
145
|
# even-dim scaling keeps the mjpeg encoder happy on odd-width sources
|
|
80
146
|
SCALE="scale=trunc(iw/2)*2:trunc(ih/2)*2"
|
|
81
147
|
|
|
82
|
-
echo "==> [$SLUG] probing"
|
|
148
|
+
echo "==> [$SLUG] probing (mode=$MODE, max_gap=${MAX_GAP}s, scene>$SCENE_THRESHOLD, frames_only=$FRAMES_ONLY)"
|
|
83
149
|
ffprobe -v error -show_entries format=duration,size:stream=codec_type,codec_name,width,height \
|
|
84
150
|
-of default=noprint_wrappers=1 "$VIDEO" | tee "$OUT/source-meta.txt"
|
|
85
151
|
DURATION="$(ffprobe -v error -show_entries format=duration -of csv=p=0 "$VIDEO" 2>/dev/null | head -1)"
|
|
86
152
|
DURATION="${DURATION:-0}"
|
|
87
153
|
|
|
88
|
-
|
|
89
|
-
|
|
154
|
+
if [ "$FRAMES_ONLY" = "1" ]; then
|
|
155
|
+
echo "==> [$SLUG] --frames-only: skipping audio + transcription"
|
|
156
|
+
else
|
|
157
|
+
echo "==> [$SLUG] extracting 16kHz mono audio"
|
|
158
|
+
ffmpeg -y -loglevel error -i "$VIDEO" -ar 16000 -ac 1 -c:a pcm_s16le "$OUT/audio.wav"
|
|
90
159
|
|
|
91
|
-
echo "==> [$SLUG] transcribing with $(basename "$MODEL") (lang=$LANG_CODE)"
|
|
92
|
-
"$WHISPER_BIN" -m "$MODEL" -f "$OUT/audio.wav" -l "$LANG_CODE" \
|
|
93
|
-
|
|
94
|
-
# audio.wav is a pure whisper intermediate (~1.9MB/min) — drop it once the transcript exists.
|
|
95
|
-
rm -f "$OUT/audio.wav"
|
|
160
|
+
echo "==> [$SLUG] transcribing with $(basename "$MODEL") (lang=$LANG_CODE)"
|
|
161
|
+
"$WHISPER_BIN" -m "$MODEL" -f "$OUT/audio.wav" -l "$LANG_CODE" \
|
|
162
|
+
--output-txt --output-srt --output-vtt --output-json -of "$OUT/transcript" -pp
|
|
163
|
+
# audio.wav is a pure whisper intermediate (~1.9MB/min) — drop it once the transcript exists.
|
|
164
|
+
rm -f "$OUT/audio.wav"
|
|
165
|
+
fi
|
|
96
166
|
|
|
97
|
-
echo "==> [$SLUG] extracting
|
|
167
|
+
echo "==> [$SLUG] extracting frames (scene change OR every ${MAX_GAP}s, whichever fires first)"
|
|
168
|
+
# ONE decode pass, time-based and frame-rate-independent:
|
|
169
|
+
# eq(n,0) always seed the first frame (also primes prev_selected_t)
|
|
170
|
+
# gt(scene,$SCENE_THRESHOLD) a scene change (slide/UI transition) fired
|
|
171
|
+
# gte(t-prev_selected_t,MAX_GAP) MAX_GAP elapsed since the last KEPT frame — guarantees
|
|
172
|
+
# coverage of static stretches scene-detect misses (app
|
|
173
|
+
# screens that change by text only). prev_selected_t is
|
|
174
|
+
# wall-clock seconds, so this is robust on variable-frame-rate
|
|
175
|
+
# screen recordings where frame-number sampling (mod(n,N)) breaks.
|
|
98
176
|
# pix_fmt yuvj420p avoids mjpeg "non full-range YUV" failures on screen recordings;
|
|
99
|
-
# even-dim scaling keeps the encoder happy on odd-width sources.
|
|
100
177
|
# metadata=print dumps each kept frame's pts_time so frames map back to the timeline.
|
|
101
178
|
ffmpeg -y -loglevel error -i "$VIDEO" \
|
|
102
|
-
-vf "select='gt(scene,$SCENE_THRESHOLD)',metadata=print:file=$OUT/frames/
|
|
103
|
-
-fps_mode vfr -pix_fmt yuvj420p -q:v 3 "$OUT/frames/
|
|
179
|
+
-vf "select='eq(n,0)+gt(scene,$SCENE_THRESHOLD)+gte(t-prev_selected_t,$MAX_GAP)',metadata=print:file=$OUT/frames/frame_times.txt,scale=trunc(iw/2)*2:trunc(ih/2)*2" \
|
|
180
|
+
-fps_mode vfr -pix_fmt yuvj420p -q:v 3 "$OUT/frames/frame_%04d.jpg" || true
|
|
181
|
+
|
|
182
|
+
# The || true above keeps a partial result usable, but zero frames means the decode
|
|
183
|
+
# produced nothing (no decodable video stream, unsupported codec, write failure). That
|
|
184
|
+
# must fail loudly — silently leaving an empty frames.json is the exact under-sampling
|
|
185
|
+
# trap issue #15 is about. Count safely (|| true so a no-match never trips set -e).
|
|
186
|
+
NSEL="$(find "$OUT/frames" -name 'frame_*.jpg' 2>/dev/null | wc -l | tr -d ' ')"
|
|
187
|
+
if [ "$NSEL" -eq 0 ]; then
|
|
188
|
+
echo "ERROR: ffmpeg extracted 0 frames from '$VIDEO'." >&2
|
|
189
|
+
echo " The file may have no decodable video stream (audio-only?), an unsupported codec," >&2
|
|
190
|
+
echo " or the output dir isn't writable. No frames.json written." >&2
|
|
191
|
+
exit 1
|
|
192
|
+
fi
|
|
104
193
|
|
|
105
194
|
echo "==> [$SLUG] capturing first + last anchor frames"
|
|
106
195
|
# Short, cut-heavy creatives carry the most information in the opening hook and the
|
|
@@ -110,27 +199,31 @@ ffmpeg -y -loglevel error -i "$VIDEO" -vf "$SCALE" -frames:v 1 -q:v 3 \
|
|
|
110
199
|
ffmpeg -y -loglevel error -sseof -0.5 -i "$VIDEO" -vf "$SCALE" -update 1 -frames:v 1 -q:v 3 \
|
|
111
200
|
"$OUT/frames/anchor_last.jpg" || true
|
|
112
201
|
|
|
113
|
-
echo "==> [$SLUG]
|
|
114
|
-
|
|
115
|
-
# (app demo, slide, talking head) gets no frame. gap_fill.py returns timestamps that
|
|
116
|
-
# keep every unsampled stretch <= MAX_GAP; we grab each and log its known pts_time.
|
|
117
|
-
: > "$OUT/frames/gap_times.txt"
|
|
118
|
-
gi=0
|
|
119
|
-
while IFS= read -r t; do
|
|
120
|
-
[ -z "$t" ] && continue
|
|
121
|
-
printf -v fn "gap_%04d.jpg" "$((gi + 1))"
|
|
122
|
-
if ffmpeg -y -loglevel error -ss "$t" -i "$VIDEO" -vf "$SCALE" -frames:v 1 -q:v 3 "$OUT/frames/$fn"; then
|
|
123
|
-
echo "frame:$gi pts_time:$t" >> "$OUT/frames/gap_times.txt"
|
|
124
|
-
gi=$((gi + 1))
|
|
125
|
-
fi
|
|
126
|
-
done < <(python3 "$SCRIPT_DIR/gap_fill.py" "$OUT" "$DURATION" "$MAX_GAP")
|
|
127
|
-
echo " gap frames added: $gi"
|
|
202
|
+
echo "==> [$SLUG] building frames.json (timestamp index, dedup near-dups, coverage check)"
|
|
203
|
+
MAX_GAP="$MAX_GAP" python3 "$SCRIPT_DIR/build_frame_index.py" "$OUT" "$DURATION"
|
|
128
204
|
|
|
129
|
-
|
|
130
|
-
|
|
205
|
+
# --- optional contact sheet: one glance to verify coverage before deep analysis --
|
|
206
|
+
if [ "$CONTACT_SHEET" = "1" ]; then
|
|
207
|
+
echo "==> [$SLUG] building contact sheet (coverage montage)"
|
|
208
|
+
NF="$(ls -1 "$OUT/frames"/frame_*.jpg 2>/dev/null | wc -l | tr -d ' ')"
|
|
209
|
+
if [ "$NF" -gt 0 ]; then
|
|
210
|
+
COLS="$(awk -v n="$NF" 'BEGIN{c=int(sqrt(n)); if(c*c<n)c++; print c}')"
|
|
211
|
+
ROWS="$(awk -v n="$NF" -v c="$COLS" 'BEGIN{r=int(n/c); if(r*c<n)r++; print r}')"
|
|
212
|
+
ffmpeg -y -loglevel error -pattern_type glob -i "$OUT/frames/frame_*.jpg" \
|
|
213
|
+
-vf "scale=240:-1,tile=${COLS}x${ROWS}:padding=4:margin=4" -frames:v 1 -q:v 4 \
|
|
214
|
+
"$OUT/frames/contact_sheet.jpg" \
|
|
215
|
+
&& echo " contact sheet: $OUT/frames/contact_sheet.jpg (${COLS}x${ROWS})" \
|
|
216
|
+
|| echo " contact sheet skipped (montage failed)"
|
|
217
|
+
fi
|
|
218
|
+
fi
|
|
131
219
|
|
|
132
|
-
|
|
220
|
+
# find -not -name keeps the count set -e-safe (grep -v exits 1 on no-match, tripping pipefail).
|
|
221
|
+
NFRAMES="$(find "$OUT/frames" -name '*.jpg' ! -name 'contact_sheet.jpg' 2>/dev/null | wc -l | tr -d ' ')"
|
|
133
222
|
echo "==> [$SLUG] DONE"
|
|
134
|
-
echo " transcript: $OUT/transcript.srt"
|
|
223
|
+
[ "$FRAMES_ONLY" = "1" ] || echo " transcript: $OUT/transcript.srt"
|
|
135
224
|
echo " frames: $NFRAMES (index: $OUT/frames.json)"
|
|
136
|
-
|
|
225
|
+
if [ "$FRAMES_ONLY" = "1" ]; then
|
|
226
|
+
echo " next: Claude reads frames.json, views key frames, writes the curated screen list / transcript.md"
|
|
227
|
+
else
|
|
228
|
+
echo " next: Claude reads transcript + frames.json, views key frames, writes the curated transcript.md"
|
|
229
|
+
fi
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{aq as o,ar as n}from"./index-flRpQtDj.js";const t=(r,a)=>o.lang.round(n.parse(r)[a]);export{t as c};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{s as a,c as s,a as e,C as t}from"./chunk-4TB4RGXK-DeVy5g6H.js";import{_ as i}from"./index-flRpQtDj.js";import"./chunk-FMBD7UC4-DKTOmvgZ.js";import"./chunk-YZCP3GAM-DbY-KoDn.js";import"./chunk-55IACEB6-DGy3ZgDZ.js";import"./chunk-EDXVE4YY-CYJehjz4.js";var u={parser:e,get db(){return new t},renderer:s,styles:a,init:i(r=>{r.class||(r.class={}),r.class.arrowMarkerAbsolute=r.arrowMarkerAbsolute},"init")};export{u as diagram};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{s as a,c as s,a as e,C as t}from"./chunk-4TB4RGXK-DeVy5g6H.js";import{_ as i}from"./index-flRpQtDj.js";import"./chunk-FMBD7UC4-DKTOmvgZ.js";import"./chunk-YZCP3GAM-DbY-KoDn.js";import"./chunk-55IACEB6-DGy3ZgDZ.js";import"./chunk-EDXVE4YY-CYJehjz4.js";var u={parser:e,get db(){return new t},renderer:s,styles:a,init:i(r=>{r.class||(r.class={}),r.class.arrowMarkerAbsolute=r.arrowMarkerAbsolute},"init")};export{u as diagram};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{b as r}from"./graph-CUxxgCtS.js";var e=4;function a(o){return r(o,e)}export{a as c};
|