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.
Files changed (80) hide show
  1. package/README.md +2 -1
  2. package/agents/sleep-product.md +19 -2
  3. package/agents/sleep-state.md +43 -22
  4. package/agents/sleep-tasks.md +18 -0
  5. package/dist/agents/sleep-product.md +19 -2
  6. package/dist/agents/sleep-state.md +43 -22
  7. package/dist/agents/sleep-tasks.md +18 -0
  8. package/dist/dashboard/assets/{BrainCanvas3D-LLqVeXtb.js → BrainCanvas3D-lFgJbbhZ.js} +1 -1
  9. package/dist/dashboard/assets/{_baseUniq-DW0uA0ty.js → _baseUniq-BpANgc_i.js} +1 -1
  10. package/dist/dashboard/assets/{arc-fVUGtlbL.js → arc-CX32Jm7E.js} +1 -1
  11. package/dist/dashboard/assets/{architectureDiagram-Q4EWVU46-Dz2PKBsS.js → architectureDiagram-Q4EWVU46-ARASlGxO.js} +1 -1
  12. package/dist/dashboard/assets/{blockDiagram-DXYQGD6D-ByWKxzhL.js → blockDiagram-DXYQGD6D-BBvsYm9E.js} +1 -1
  13. package/dist/dashboard/assets/{c4Diagram-AHTNJAMY-dpHsVM3D.js → c4Diagram-AHTNJAMY-ChSUfXR9.js} +1 -1
  14. package/dist/dashboard/assets/channel-w_Bp182N.js +1 -0
  15. package/dist/dashboard/assets/{chunk-4BX2VUAB-HEXb6Yg5.js → chunk-4BX2VUAB-COHoVEpt.js} +1 -1
  16. package/dist/dashboard/assets/{chunk-4TB4RGXK-DeVy5g6H.js → chunk-4TB4RGXK-DKJFLaTC.js} +1 -1
  17. package/dist/dashboard/assets/{chunk-55IACEB6-DGy3ZgDZ.js → chunk-55IACEB6-CKZPWZRu.js} +1 -1
  18. package/dist/dashboard/assets/{chunk-EDXVE4YY-CYJehjz4.js → chunk-EDXVE4YY-k1y4mGUJ.js} +1 -1
  19. package/dist/dashboard/assets/{chunk-FMBD7UC4-DKTOmvgZ.js → chunk-FMBD7UC4-ixXe5R10.js} +1 -1
  20. package/dist/dashboard/assets/{chunk-OYMX7WX6-mmtWyiDA.js → chunk-OYMX7WX6-NDFap7xg.js} +1 -1
  21. package/dist/dashboard/assets/{chunk-QZHKN3VN-DSdV0iwY.js → chunk-QZHKN3VN-vY0UpHjB.js} +1 -1
  22. package/dist/dashboard/assets/{chunk-YZCP3GAM-DbY-KoDn.js → chunk-YZCP3GAM-yztvsKR-.js} +1 -1
  23. package/dist/dashboard/assets/classDiagram-6PBFFD2Q-CcQxcqy9.js +1 -0
  24. package/dist/dashboard/assets/classDiagram-v2-HSJHXN6E-CcQxcqy9.js +1 -0
  25. package/dist/dashboard/assets/clone-_znoR_ci.js +1 -0
  26. package/dist/dashboard/assets/{cose-bilkent-S5V4N54A-p0t1DY88.js → cose-bilkent-S5V4N54A-DKXM5Fh5.js} +1 -1
  27. package/dist/dashboard/assets/{dagre-KV5264BT-DYE0WzHM.js → dagre-KV5264BT-DN1Nlsmy.js} +1 -1
  28. package/dist/dashboard/assets/{diagram-5BDNPKRD-D9EiQCOP.js → diagram-5BDNPKRD-DnoJFRqR.js} +1 -1
  29. package/dist/dashboard/assets/{diagram-G4DWMVQ6-poDXSAfu.js → diagram-G4DWMVQ6-CF1_jTNI.js} +1 -1
  30. package/dist/dashboard/assets/{diagram-MMDJMWI5-CHZ7wgN1.js → diagram-MMDJMWI5-D4-bZZEt.js} +1 -1
  31. package/dist/dashboard/assets/{diagram-TYMM5635-DVk6fYG7.js → diagram-TYMM5635-al8RqVgf.js} +1 -1
  32. package/dist/dashboard/assets/{erDiagram-SMLLAGMA-D2sqkGin.js → erDiagram-SMLLAGMA-MEcC0rwO.js} +1 -1
  33. package/dist/dashboard/assets/{flowDiagram-DWJPFMVM-DrMlKBYA.js → flowDiagram-DWJPFMVM-DCLyNpW8.js} +1 -1
  34. package/dist/dashboard/assets/{ganttDiagram-T4ZO3ILL-Cf4FbEFp.js → ganttDiagram-T4ZO3ILL-DfEvbbJK.js} +1 -1
  35. package/dist/dashboard/assets/{gitGraphDiagram-UUTBAWPF-BsbOq1C9.js → gitGraphDiagram-UUTBAWPF-C_YozdVL.js} +1 -1
  36. package/dist/dashboard/assets/{graph-CUxxgCtS.js → graph-DpIXS1G1.js} +1 -1
  37. package/dist/dashboard/assets/index-Bo5CUa_M.js +480 -0
  38. package/dist/dashboard/assets/index-DrqurW1c.css +1 -0
  39. package/dist/dashboard/assets/{infoDiagram-42DDH7IO-DTkMnZiD.js → infoDiagram-42DDH7IO-AdT6kjzj.js} +1 -1
  40. package/dist/dashboard/assets/{ishikawaDiagram-UXIWVN3A-CahQ348K.js → ishikawaDiagram-UXIWVN3A-B0_9IZVO.js} +1 -1
  41. package/dist/dashboard/assets/{journeyDiagram-VCZTEJTY-BDY2Nslc.js → journeyDiagram-VCZTEJTY-BveNBswQ.js} +1 -1
  42. package/dist/dashboard/assets/{kanban-definition-6JOO6SKY-CgrwoTjI.js → kanban-definition-6JOO6SKY-CFI8j4jR.js} +1 -1
  43. package/dist/dashboard/assets/{layout-IaUxkFkm.js → layout-DI7XjZy1.js} +1 -1
  44. package/dist/dashboard/assets/{linear-shc0iNFn.js → linear-CSjp56iw.js} +1 -1
  45. package/dist/dashboard/assets/{min-BIL7YgTN.js → min-pAGUJmEC.js} +1 -1
  46. package/dist/dashboard/assets/{mindmap-definition-QFDTVHPH-GmO0JRtA.js → mindmap-definition-QFDTVHPH-C2mBnknr.js} +1 -1
  47. package/dist/dashboard/assets/{pieDiagram-DEJITSTG-Bfm_toYA.js → pieDiagram-DEJITSTG-i_phWDqD.js} +1 -1
  48. package/dist/dashboard/assets/{quadrantDiagram-34T5L4WZ-DIjTM3lm.js → quadrantDiagram-34T5L4WZ-Dv7TGJjw.js} +1 -1
  49. package/dist/dashboard/assets/{requirementDiagram-MS252O5E-B9AEoGYh.js → requirementDiagram-MS252O5E-CB2Jl-O5.js} +1 -1
  50. package/dist/dashboard/assets/{sankeyDiagram-XADWPNL6-C_O5XLXm.js → sankeyDiagram-XADWPNL6-DxCoN-EI.js} +1 -1
  51. package/dist/dashboard/assets/{sequenceDiagram-FGHM5R23-CHSxSvmJ.js → sequenceDiagram-FGHM5R23-BeZyaehJ.js} +1 -1
  52. package/dist/dashboard/assets/{stateDiagram-FHFEXIEX-B0URrhHw.js → stateDiagram-FHFEXIEX-D3AjQzD1.js} +1 -1
  53. package/dist/dashboard/assets/stateDiagram-v2-QKLJ7IA2-Ci9v--xK.js +1 -0
  54. package/dist/dashboard/assets/{timeline-definition-GMOUNBTQ-CycU2gVC.js → timeline-definition-GMOUNBTQ-3l_8RFUg.js} +1 -1
  55. package/dist/dashboard/assets/{vennDiagram-DHZGUBPP-DcU2536G.js → vennDiagram-DHZGUBPP-2QiY25JD.js} +1 -1
  56. package/dist/dashboard/assets/{wardley-RL74JXVD-BWqqaX3S.js → wardley-RL74JXVD-DNLmFofz.js} +1 -1
  57. package/dist/dashboard/assets/{wardleyDiagram-NUSXRM2D-CUnIHJgd.js → wardleyDiagram-NUSXRM2D-BI0tupiS.js} +1 -1
  58. package/dist/dashboard/assets/{xychartDiagram-5P7HB3ND-b9fcOYTj.js → xychartDiagram-5P7HB3ND-CghXPE7_.js} +1 -1
  59. package/dist/dashboard/index.html +2 -2
  60. package/dist/index.js +1669 -1064
  61. package/dist/skill-packs/excalidraw/SKILL.md +82 -3
  62. package/dist/skill-packs/video-watching/SKILL.md +54 -13
  63. package/dist/skill-packs/video-watching/scripts/build_frame_index.py +61 -10
  64. package/dist/skill-packs/video-watching/scripts/transcribe.sh +147 -54
  65. package/dist/templates/init/data-structures/default.md +26 -24
  66. package/package.json +1 -1
  67. package/skill/SKILL.md +26 -12
  68. package/skill-packs/excalidraw/SKILL.md +82 -3
  69. package/skill-packs/video-watching/SKILL.md +54 -13
  70. package/skill-packs/video-watching/scripts/build_frame_index.py +61 -10
  71. package/skill-packs/video-watching/scripts/transcribe.sh +147 -54
  72. package/dist/dashboard/assets/channel-zrLwggBX.js +0 -1
  73. package/dist/dashboard/assets/classDiagram-6PBFFD2Q-BvejNiwH.js +0 -1
  74. package/dist/dashboard/assets/classDiagram-v2-HSJHXN6E-BvejNiwH.js +0 -1
  75. package/dist/dashboard/assets/clone-BFVdml6g.js +0 -1
  76. package/dist/dashboard/assets/index-CqSkXBSu.css +0 -1
  77. package/dist/dashboard/assets/index-flRpQtDj.js +0 -476
  78. package/dist/dashboard/assets/stateDiagram-v2-QKLJ7IA2-FdAb9MBo.js +0 -1
  79. package/dist/skill-packs/video-watching/scripts/gap_fill.py +0 -45
  80. 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
- # SCENE_THRESHOLD=0.15 ffmpeg scene-change sensitivity (lower = more frames)
17
- # MAX_GAP=10 guarantee a frame at least every N seconds (fills static sections)
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/scene_*.jpg frames at scene-change > SCENE_THRESHOLD
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
- VIDEO="${1:?Usage: transcribe.sh <video> [lang]}"
30
- LANG_CODE="${2:-auto}" # whisper auto-detects the spoken language unless overridden
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
- MAX_GAP="${MAX_GAP:-10}"
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
- MODEL="$(pick_model)"
61
- [ -n "$MODEL" ] && [ -f "$MODEL" ] || {
62
- echo "no whisper model found. Install one, e.g.:" >&2
63
- echo " curl -L -o ~/.cache/whisper.cpp/models/ggml-large-v3-turbo.bin \\" >&2
64
- echo " https://huggingface.co/ggerganov/whisper.cpp/resolve/main/ggml-large-v3-turbo.bin" >&2
65
- echo " or set WHISPER_MODEL=/abs/path/ggml-*.bin" >&2
66
- exit 1
67
- }
68
-
69
- WHISPER_BIN="$(command -v whisper-cli || command -v whisper-cpp || command -v main || true)"
70
- [ -n "$WHISPER_BIN" ] || { echo "whisper.cpp binary not found (brew install whisper-cpp)" >&2; exit 1; }
71
- command -v ffmpeg >/dev/null || { echo "ffmpeg not found (brew install ffmpeg)" >&2; exit 1; }
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
- echo "==> [$SLUG] extracting 16kHz mono audio"
89
- ffmpeg -y -loglevel error -i "$VIDEO" -ar 16000 -ac 1 -c:a pcm_s16le "$OUT/audio.wav"
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
- --output-txt --output-srt --output-vtt --output-json -of "$OUT/transcript" -pp
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 scene-change frames (scene > $SCENE_THRESHOLD)"
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/scene_times.txt,scale=trunc(iw/2)*2:trunc(ih/2)*2" \
103
- -fps_mode vfr -pix_fmt yuvj420p -q:v 3 "$OUT/frames/scene_%04d.jpg" || true
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] filling gaps > ${MAX_GAP}s (static sections scene-detect misses)"
114
- # scene-detect fires on motion, not meaning; a static-but-information-rich stretch
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
- echo "==> [$SLUG] building frames.json (timestamp index, dedup near-dups)"
130
- python3 "$SCRIPT_DIR/build_frame_index.py" "$OUT" "$DURATION"
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
- NFRAMES="$(ls -1 "$OUT/frames"/*.jpg 2>/dev/null | wc -l | tr -d ' ')"
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
- echo " next: Claude reads transcript + frames.json, views key frames, writes the curated transcript.md"
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};