dreamcontext 0.5.4 → 0.6.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/LICENSE +201 -21
- package/NOTICE +7 -0
- package/README.md +77 -12
- package/agents/sleep-product.md +29 -8
- package/agents/sleep-tasks.md +44 -9
- package/dist/agents/sleep-product.md +29 -8
- package/dist/agents/sleep-tasks.md +44 -9
- package/dist/dashboard/assets/{BrainCanvas3D-1HMiA14F.js → BrainCanvas3D-LLqVeXtb.js} +1 -1
- package/dist/dashboard/assets/{_baseUniq-C7pfRmXz.js → _baseUniq-DW0uA0ty.js} +1 -1
- package/dist/dashboard/assets/{arc-B5x_GF9Q.js → arc-fVUGtlbL.js} +1 -1
- package/dist/dashboard/assets/{architectureDiagram-Q4EWVU46-CWmjaqno.js → architectureDiagram-Q4EWVU46-Dz2PKBsS.js} +1 -1
- package/dist/dashboard/assets/{blockDiagram-DXYQGD6D-B7SyUIdV.js → blockDiagram-DXYQGD6D-ByWKxzhL.js} +1 -1
- package/dist/dashboard/assets/{c4Diagram-AHTNJAMY-OeJBWXHU.js → c4Diagram-AHTNJAMY-dpHsVM3D.js} +1 -1
- package/dist/dashboard/assets/channel-zrLwggBX.js +1 -0
- package/dist/dashboard/assets/{chunk-4BX2VUAB-2Oop1B0M.js → chunk-4BX2VUAB-HEXb6Yg5.js} +1 -1
- package/dist/dashboard/assets/{chunk-4TB4RGXK-Bh7qSu_X.js → chunk-4TB4RGXK-DeVy5g6H.js} +1 -1
- package/dist/dashboard/assets/{chunk-55IACEB6-gNeHRO_i.js → chunk-55IACEB6-DGy3ZgDZ.js} +1 -1
- package/dist/dashboard/assets/{chunk-EDXVE4YY-BRniAH53.js → chunk-EDXVE4YY-CYJehjz4.js} +1 -1
- package/dist/dashboard/assets/{chunk-FMBD7UC4-B5r8U2gJ.js → chunk-FMBD7UC4-DKTOmvgZ.js} +1 -1
- package/dist/dashboard/assets/{chunk-OYMX7WX6-Dd1c17fm.js → chunk-OYMX7WX6-mmtWyiDA.js} +1 -1
- package/dist/dashboard/assets/{chunk-QZHKN3VN-BIw-OdTn.js → chunk-QZHKN3VN-DSdV0iwY.js} +1 -1
- package/dist/dashboard/assets/{chunk-YZCP3GAM-Dl9MlwCW.js → chunk-YZCP3GAM-DbY-KoDn.js} +1 -1
- package/dist/dashboard/assets/classDiagram-6PBFFD2Q-BvejNiwH.js +1 -0
- package/dist/dashboard/assets/classDiagram-v2-HSJHXN6E-BvejNiwH.js +1 -0
- package/dist/dashboard/assets/clone-BFVdml6g.js +1 -0
- package/dist/dashboard/assets/{cose-bilkent-S5V4N54A-DlrROH5K.js → cose-bilkent-S5V4N54A-p0t1DY88.js} +1 -1
- package/dist/dashboard/assets/{dagre-KV5264BT-B0GpPXKL.js → dagre-KV5264BT-DYE0WzHM.js} +1 -1
- package/dist/dashboard/assets/{diagram-5BDNPKRD-BBYcDW2Z.js → diagram-5BDNPKRD-D9EiQCOP.js} +1 -1
- package/dist/dashboard/assets/{diagram-G4DWMVQ6-BxHxugdp.js → diagram-G4DWMVQ6-poDXSAfu.js} +1 -1
- package/dist/dashboard/assets/{diagram-MMDJMWI5-C9Jc19Wo.js → diagram-MMDJMWI5-CHZ7wgN1.js} +1 -1
- package/dist/dashboard/assets/{diagram-TYMM5635-Vmo5GAAU.js → diagram-TYMM5635-DVk6fYG7.js} +1 -1
- package/dist/dashboard/assets/{erDiagram-SMLLAGMA-pskr9x-V.js → erDiagram-SMLLAGMA-D2sqkGin.js} +1 -1
- package/dist/dashboard/assets/{flowDiagram-DWJPFMVM-B1D7Ky0W.js → flowDiagram-DWJPFMVM-DrMlKBYA.js} +1 -1
- package/dist/dashboard/assets/{ganttDiagram-T4ZO3ILL-BVJZZV1M.js → ganttDiagram-T4ZO3ILL-Cf4FbEFp.js} +1 -1
- package/dist/dashboard/assets/{gitGraphDiagram-UUTBAWPF-BiOGN6We.js → gitGraphDiagram-UUTBAWPF-BsbOq1C9.js} +1 -1
- package/dist/dashboard/assets/{graph-D58deEXr.js → graph-CUxxgCtS.js} +1 -1
- package/dist/dashboard/assets/index-CqSkXBSu.css +1 -0
- package/dist/dashboard/assets/index-flRpQtDj.js +476 -0
- package/dist/dashboard/assets/{infoDiagram-42DDH7IO-DgJYg1X1.js → infoDiagram-42DDH7IO-DTkMnZiD.js} +1 -1
- package/dist/dashboard/assets/{ishikawaDiagram-UXIWVN3A-arJgr-8P.js → ishikawaDiagram-UXIWVN3A-CahQ348K.js} +1 -1
- package/dist/dashboard/assets/{journeyDiagram-VCZTEJTY-B3DCvb8p.js → journeyDiagram-VCZTEJTY-BDY2Nslc.js} +1 -1
- package/dist/dashboard/assets/{kanban-definition-6JOO6SKY-B--dN6Ma.js → kanban-definition-6JOO6SKY-CgrwoTjI.js} +1 -1
- package/dist/dashboard/assets/{layout-z2qEbZWC.js → layout-IaUxkFkm.js} +1 -1
- package/dist/dashboard/assets/{linear-BQu1dwrM.js → linear-shc0iNFn.js} +1 -1
- package/dist/dashboard/assets/{min-DkF5b2sh.js → min-BIL7YgTN.js} +1 -1
- package/dist/dashboard/assets/{mindmap-definition-QFDTVHPH-Bw04koyq.js → mindmap-definition-QFDTVHPH-GmO0JRtA.js} +1 -1
- package/dist/dashboard/assets/{pieDiagram-DEJITSTG-vNqz64o8.js → pieDiagram-DEJITSTG-Bfm_toYA.js} +1 -1
- package/dist/dashboard/assets/{quadrantDiagram-34T5L4WZ-TuVP5Gi6.js → quadrantDiagram-34T5L4WZ-DIjTM3lm.js} +1 -1
- package/dist/dashboard/assets/{requirementDiagram-MS252O5E-D3mS8_ke.js → requirementDiagram-MS252O5E-B9AEoGYh.js} +1 -1
- package/dist/dashboard/assets/{sankeyDiagram-XADWPNL6-BmVRers6.js → sankeyDiagram-XADWPNL6-C_O5XLXm.js} +1 -1
- package/dist/dashboard/assets/{sequenceDiagram-FGHM5R23-CTYCgk5P.js → sequenceDiagram-FGHM5R23-CHSxSvmJ.js} +1 -1
- package/dist/dashboard/assets/{stateDiagram-FHFEXIEX-DNbP2aCg.js → stateDiagram-FHFEXIEX-B0URrhHw.js} +1 -1
- package/dist/dashboard/assets/stateDiagram-v2-QKLJ7IA2-FdAb9MBo.js +1 -0
- package/dist/dashboard/assets/{timeline-definition-GMOUNBTQ-DKHJd4z8.js → timeline-definition-GMOUNBTQ-CycU2gVC.js} +1 -1
- package/dist/dashboard/assets/{vennDiagram-DHZGUBPP-ChotNMNq.js → vennDiagram-DHZGUBPP-DcU2536G.js} +1 -1
- package/dist/dashboard/assets/{wardley-RL74JXVD-Dmo2_ksP.js → wardley-RL74JXVD-BWqqaX3S.js} +1 -1
- package/dist/dashboard/assets/{wardleyDiagram-NUSXRM2D-Z6Wbiv2H.js → wardleyDiagram-NUSXRM2D-CUnIHJgd.js} +1 -1
- package/dist/dashboard/assets/{xychartDiagram-5P7HB3ND-BJa0AHtN.js → xychartDiagram-5P7HB3ND-b9fcOYTj.js} +1 -1
- package/dist/dashboard/index.html +2 -2
- package/dist/dashboard/media/README.md +29 -0
- package/dist/dashboard/media/brain-hero.png +0 -0
- package/dist/dashboard/media/brain.mp4 +0 -0
- package/dist/dashboard/media/brain.webm +0 -0
- package/dist/dashboard/media/shot-disabled.png +0 -0
- package/dist/dashboard/media/shot-enabled.png +0 -0
- package/dist/index.js +2280 -1568
- package/dist/skill-packs/catalog.json +44 -0
- package/dist/skill-packs/excalidraw/SKILL.md +123 -0
- package/dist/skill-packs/excalidraw/examples/hello.spec.json +14 -0
- package/dist/skill-packs/excalidraw/examples/sample.png +0 -0
- package/dist/skill-packs/excalidraw/examples/style_board.js +31 -0
- package/dist/skill-packs/excalidraw/package.json +5 -0
- package/dist/skill-packs/excalidraw/reference/format.md +93 -0
- package/dist/skill-packs/excalidraw/scripts/build_excalidraw.js +357 -0
- package/dist/skill-packs/excalidraw/scripts/lib/fractional-indexing.LICENSE +121 -0
- package/dist/skill-packs/excalidraw/scripts/lib/fractional-indexing.js +311 -0
- package/dist/skill-packs/excalidraw/scripts/lib/imagesize.js +65 -0
- package/dist/skill-packs/excalidraw/scripts/lib/style.js +130 -0
- package/dist/skill-packs/video-watching/SKILL.md +151 -0
- package/dist/skill-packs/video-watching/scripts/build_frame_index.py +95 -0
- package/dist/skill-packs/video-watching/scripts/gap_fill.py +45 -0
- package/dist/skill-packs/video-watching/scripts/transcribe.sh +136 -0
- package/package.json +4 -2
- package/skill/SKILL.md +5 -0
- package/skill-packs/catalog.json +44 -0
- package/skill-packs/excalidraw/SKILL.md +123 -0
- package/skill-packs/excalidraw/examples/hello.spec.json +14 -0
- package/skill-packs/excalidraw/examples/sample.png +0 -0
- package/skill-packs/excalidraw/examples/style_board.js +31 -0
- package/skill-packs/excalidraw/package.json +5 -0
- package/skill-packs/excalidraw/reference/format.md +93 -0
- package/skill-packs/excalidraw/scripts/build_excalidraw.js +357 -0
- package/skill-packs/excalidraw/scripts/lib/fractional-indexing.LICENSE +121 -0
- package/skill-packs/excalidraw/scripts/lib/fractional-indexing.js +311 -0
- package/skill-packs/excalidraw/scripts/lib/imagesize.js +65 -0
- package/skill-packs/excalidraw/scripts/lib/style.js +130 -0
- package/skill-packs/video-watching/SKILL.md +151 -0
- package/skill-packs/video-watching/scripts/build_frame_index.py +95 -0
- package/skill-packs/video-watching/scripts/gap_fill.py +45 -0
- package/skill-packs/video-watching/scripts/transcribe.sh +136 -0
- package/dist/dashboard/assets/channel-CbuTKKsM.js +0 -1
- package/dist/dashboard/assets/classDiagram-6PBFFD2Q-HXl3A_sM.js +0 -1
- package/dist/dashboard/assets/classDiagram-v2-HSJHXN6E-HXl3A_sM.js +0 -1
- package/dist/dashboard/assets/clone-NKHOtdi8.js +0 -1
- package/dist/dashboard/assets/index-DK_2-eWY.css +0 -1
- package/dist/dashboard/assets/index-Ds-JXWtr.js +0 -476
- package/dist/dashboard/assets/stateDiagram-v2-QKLJ7IA2-CfN6kjo8.js +0 -1
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Build frames.json: map each extracted frame to its pts_time on the video timeline.
|
|
3
|
+
|
|
4
|
+
Reads the per-frame pts_time dumps (scene_times.txt from ffmpeg metadata, gap_times.txt
|
|
5
|
+
written by transcribe.sh) alongside the frames, pairs each timestamp with its
|
|
6
|
+
zero-padded frame file in selection order, adds the first/last anchor frames, drops
|
|
7
|
+
near-duplicate timestamps, and emits frames.json sorted by time.
|
|
8
|
+
|
|
9
|
+
Stdlib only — no venv needed.
|
|
10
|
+
Usage: build_frame_index.py <OUT_DIR> [DURATION_SEC]
|
|
11
|
+
Env: DEDUPE_SEC=0.4 collapse non-anchor frames closer than this to the kept one.
|
|
12
|
+
"""
|
|
13
|
+
import json
|
|
14
|
+
import os
|
|
15
|
+
import re
|
|
16
|
+
import sys
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
# metadata=print header line, e.g.: "frame:0 pts:321024 pts_time:12.852500"
|
|
20
|
+
FRAME_RE = re.compile(r"frame:(\d+)\b.*?pts_time:([\d.]+)", re.DOTALL)
|
|
21
|
+
DEDUPE_SEC = float(os.environ.get("DEDUPE_SEC", "0.4"))
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def parse_times(meta_file: Path) -> dict[int, float]:
|
|
25
|
+
"""frame index (0-based, as ffmpeg numbers kept frames) -> pts_time seconds."""
|
|
26
|
+
if not meta_file.exists():
|
|
27
|
+
return {}
|
|
28
|
+
text = meta_file.read_text(errors="replace")
|
|
29
|
+
return {int(n): round(float(t), 2) for n, t in FRAME_RE.findall(text)}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def collect(out_dir: Path, prefix: str, meta_name: str, kind: str) -> list[dict]:
|
|
33
|
+
times = parse_times(out_dir / "frames" / meta_name)
|
|
34
|
+
frames = sorted((out_dir / "frames").glob(f"{prefix}_*.jpg"))
|
|
35
|
+
# transcribe.sh names files 1-based (%04d starts at 0001); metadata frame: is 0-based.
|
|
36
|
+
return [{"file": f"frames/{f.name}", "t": times.get(i), "type": kind}
|
|
37
|
+
for i, f in enumerate(frames)]
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def anchors(out_dir: Path, duration: float) -> list[dict]:
|
|
41
|
+
rows = []
|
|
42
|
+
if (out_dir / "frames" / "anchor_first.jpg").exists():
|
|
43
|
+
rows.append({"file": "frames/anchor_first.jpg", "t": 0.0, "type": "anchor"})
|
|
44
|
+
if (out_dir / "frames" / "anchor_last.jpg").exists():
|
|
45
|
+
t = round(duration, 2) if duration > 0 else None
|
|
46
|
+
rows.append({"file": "frames/anchor_last.jpg", "t": t, "type": "anchor"})
|
|
47
|
+
return rows
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def fmt(t):
|
|
51
|
+
if t is None:
|
|
52
|
+
return "??:??"
|
|
53
|
+
m, s = divmod(int(t), 60)
|
|
54
|
+
return f"{m:02d}:{s:02d}"
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def dedupe(rows: list[dict]) -> list[dict]:
|
|
58
|
+
"""Keep all anchors; drop a non-anchor frame within DEDUPE_SEC of the last kept."""
|
|
59
|
+
kept: list[dict] = []
|
|
60
|
+
last_t = None
|
|
61
|
+
for r in rows:
|
|
62
|
+
is_anchor = r["type"] == "anchor"
|
|
63
|
+
if (not is_anchor and last_t is not None and r["t"] is not None
|
|
64
|
+
and abs(r["t"] - last_t) < DEDUPE_SEC):
|
|
65
|
+
continue
|
|
66
|
+
kept.append(r)
|
|
67
|
+
if r["t"] is not None:
|
|
68
|
+
last_t = r["t"]
|
|
69
|
+
return kept
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def main() -> int:
|
|
73
|
+
if len(sys.argv) < 2:
|
|
74
|
+
print("usage: build_frame_index.py <OUT_DIR> [DURATION_SEC]", file=sys.stderr)
|
|
75
|
+
return 2
|
|
76
|
+
out_dir = Path(sys.argv[1])
|
|
77
|
+
duration = float(sys.argv[2]) if len(sys.argv) > 2 else 0.0
|
|
78
|
+
|
|
79
|
+
rows = collect(out_dir, "scene", "scene_times.txt", "scene")
|
|
80
|
+
rows += collect(out_dir, "gap", "gap_times.txt", "gap")
|
|
81
|
+
rows += anchors(out_dir, duration)
|
|
82
|
+
# Sort by time; unknown timestamps sink to the end.
|
|
83
|
+
rows.sort(key=lambda r: (r["t"] is None, r["t"] or 0.0))
|
|
84
|
+
rows = dedupe(rows)
|
|
85
|
+
for r in rows:
|
|
86
|
+
r["at"] = fmt(r["t"])
|
|
87
|
+
|
|
88
|
+
(out_dir / "frames.json").write_text(json.dumps(rows, ensure_ascii=False, indent=2))
|
|
89
|
+
n_anchor = sum(r["type"] == "anchor" for r in rows)
|
|
90
|
+
print(f" frames.json: {len(rows)} frames indexed ({n_anchor} anchors, dedupe<{DEDUPE_SEC}s)")
|
|
91
|
+
return 0
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
if __name__ == "__main__":
|
|
95
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Print timestamps that keep every unsampled stretch of the video <= MAX_GAP.
|
|
3
|
+
|
|
4
|
+
Scene-detect fires on motion, not on meaning — so static-but-information-rich
|
|
5
|
+
sections (app demos, slides, talking heads) get no frame. This reads the scene
|
|
6
|
+
timestamps, bounds them with 0 and the video duration, and for any gap larger than
|
|
7
|
+
MAX_GAP emits evenly-spaced fill timestamps so no stretch goes unsampled.
|
|
8
|
+
|
|
9
|
+
Stdlib only. Usage: gap_fill.py <OUT_DIR> <DURATION_SEC> [MAX_GAP_SEC]
|
|
10
|
+
Prints one timestamp (seconds, 2dp) per line — transcribe.sh grabs each frame.
|
|
11
|
+
"""
|
|
12
|
+
import re
|
|
13
|
+
import sys
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
FRAME_RE = re.compile(r"frame:(\d+)\b.*?pts_time:([\d.]+)", re.DOTALL)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def main() -> int:
|
|
20
|
+
if len(sys.argv) < 3:
|
|
21
|
+
print("usage: gap_fill.py <OUT_DIR> <DURATION_SEC> [MAX_GAP_SEC]", file=sys.stderr)
|
|
22
|
+
return 2
|
|
23
|
+
out_dir = Path(sys.argv[1])
|
|
24
|
+
duration = float(sys.argv[2])
|
|
25
|
+
max_gap = float(sys.argv[3]) if len(sys.argv) > 3 else 10.0
|
|
26
|
+
if duration <= 0 or max_gap <= 0:
|
|
27
|
+
return 0
|
|
28
|
+
|
|
29
|
+
meta = out_dir / "frames" / "scene_times.txt"
|
|
30
|
+
scene_times = []
|
|
31
|
+
if meta.exists():
|
|
32
|
+
scene_times = [round(float(t), 2)
|
|
33
|
+
for _, t in FRAME_RE.findall(meta.read_text(errors="replace"))]
|
|
34
|
+
|
|
35
|
+
bounds = sorted(set([0.0] + scene_times + [round(duration, 2)]))
|
|
36
|
+
for a, b in zip(bounds, bounds[1:]):
|
|
37
|
+
gap = b - a
|
|
38
|
+
n = int(gap // max_gap) # inserts that split the gap into <= max_gap pieces
|
|
39
|
+
for k in range(1, n + 1):
|
|
40
|
+
print(f"{a + gap * k / (n + 1):.2f}")
|
|
41
|
+
return 0
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
if __name__ == "__main__":
|
|
45
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Video -> time-mapped transcript + significant frames (with per-frame timestamps).
|
|
3
|
+
# whisper.cpp (large-v3-turbo by default) for the transcript, ffmpeg for frames.
|
|
4
|
+
#
|
|
5
|
+
# This is the MECHANICAL step. It produces artifacts; it does NOT understand the
|
|
6
|
+
# video. Claude reads the outputs (transcript + frames.json), decides which frames
|
|
7
|
+
# matter, views them, and writes the curated <video>.transcript.md (see SKILL.md).
|
|
8
|
+
#
|
|
9
|
+
# Usage: ./transcribe.sh "/abs/path/to/video.mov" [lang]
|
|
10
|
+
# lang: whisper language code. DEFAULT 'auto' — whisper detects it, no need to pass.
|
|
11
|
+
# Force only if auto mislabels a short/ambiguous clip (e.g. 'tr', 'en').
|
|
12
|
+
#
|
|
13
|
+
# Env overrides:
|
|
14
|
+
# WHISPER_MODEL=/abs/path/ggml-*.bin force a specific model
|
|
15
|
+
# 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)
|
|
18
|
+
#
|
|
19
|
+
# 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
|
|
24
|
+
# frames/anchor_first.jpg / anchor_last.jpg always-captured first + last frame
|
|
25
|
+
# frames.json [{file, t, at, type}] — each frame's pts_time, sorted (Claude reads this)
|
|
26
|
+
# source-meta.txt ffprobe dump (audio.wav is created then deleted after transcription)
|
|
27
|
+
set -euo pipefail
|
|
28
|
+
|
|
29
|
+
VIDEO="${1:?Usage: transcribe.sh <video> [lang]}"
|
|
30
|
+
LANG_CODE="${2:-auto}" # whisper auto-detects the spoken language unless overridden
|
|
31
|
+
SCENE_THRESHOLD="${SCENE_THRESHOLD:-0.15}"
|
|
32
|
+
MAX_GAP="${MAX_GAP:-10}"
|
|
33
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
34
|
+
|
|
35
|
+
# --- remote link? download with yt-dlp (local files skip this) ------------------
|
|
36
|
+
if [[ "$VIDEO" =~ ^https?:// ]]; then
|
|
37
|
+
command -v yt-dlp >/dev/null || { echo "remote link needs yt-dlp: brew install yt-dlp" >&2; exit 1; }
|
|
38
|
+
DL_DIR="${OUT_DIR:-$PWD}/_download"; mkdir -p "$DL_DIR"
|
|
39
|
+
echo "==> downloading remote video with yt-dlp"
|
|
40
|
+
yt-dlp -o "$DL_DIR/%(title)s.%(ext)s" "$VIDEO"
|
|
41
|
+
VIDEO="$(ls -t "$DL_DIR"/* | head -1)"
|
|
42
|
+
echo "==> downloaded: $VIDEO"
|
|
43
|
+
fi
|
|
44
|
+
[ -f "$VIDEO" ] || { echo "video not found: $VIDEO" >&2; exit 1; }
|
|
45
|
+
|
|
46
|
+
# --- model: prefer turbo, then large-v3, then medium (override with WHISPER_MODEL)
|
|
47
|
+
pick_model() {
|
|
48
|
+
if [ -n "${WHISPER_MODEL:-}" ]; then printf '%s' "$WHISPER_MODEL"; return; fi
|
|
49
|
+
local p
|
|
50
|
+
for p in \
|
|
51
|
+
"$HOME/.cache/whisper.cpp/models/ggml-large-v3-turbo.bin" \
|
|
52
|
+
"$HOME/.cache/openwhispr/whisper-models/ggml-large-v3-turbo.bin" \
|
|
53
|
+
"$HOME/.cache/whisper.cpp/models/ggml-large-v3.bin" \
|
|
54
|
+
"$HOME/.cache/whisper.cpp/models/ggml-medium.bin" \
|
|
55
|
+
"$HOME/.cache/openwhispr/whisper-models/ggml-medium.bin"; do
|
|
56
|
+
[ -f "$p" ] && { printf '%s' "$p"; return; }
|
|
57
|
+
done
|
|
58
|
+
printf ''
|
|
59
|
+
}
|
|
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; }
|
|
72
|
+
|
|
73
|
+
# --- where artifacts go: next to the video by default ---------------------------
|
|
74
|
+
SRC_DIR="$(cd "$(dirname "$VIDEO")" && pwd)"
|
|
75
|
+
SLUG="$(basename "$VIDEO")"; SLUG="${SLUG%.*}"
|
|
76
|
+
SLUG="$(echo "$SLUG" | tr ' ' '-' | tr -cd '[:alnum:]._-' | sed 's/--*/-/g; s/^-//; s/-$//')"
|
|
77
|
+
OUT="${OUT_DIR:-$SRC_DIR/$SLUG.media}"
|
|
78
|
+
mkdir -p "$OUT/frames"
|
|
79
|
+
# even-dim scaling keeps the mjpeg encoder happy on odd-width sources
|
|
80
|
+
SCALE="scale=trunc(iw/2)*2:trunc(ih/2)*2"
|
|
81
|
+
|
|
82
|
+
echo "==> [$SLUG] probing"
|
|
83
|
+
ffprobe -v error -show_entries format=duration,size:stream=codec_type,codec_name,width,height \
|
|
84
|
+
-of default=noprint_wrappers=1 "$VIDEO" | tee "$OUT/source-meta.txt"
|
|
85
|
+
DURATION="$(ffprobe -v error -show_entries format=duration -of csv=p=0 "$VIDEO" 2>/dev/null | head -1)"
|
|
86
|
+
DURATION="${DURATION:-0}"
|
|
87
|
+
|
|
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"
|
|
90
|
+
|
|
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"
|
|
96
|
+
|
|
97
|
+
echo "==> [$SLUG] extracting scene-change frames (scene > $SCENE_THRESHOLD)"
|
|
98
|
+
# 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
|
+
# metadata=print dumps each kept frame's pts_time so frames map back to the timeline.
|
|
101
|
+
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
|
|
104
|
+
|
|
105
|
+
echo "==> [$SLUG] capturing first + last anchor frames"
|
|
106
|
+
# Short, cut-heavy creatives carry the most information in the opening hook and the
|
|
107
|
+
# closing CTA — but scene-detect rarely fires at t=0 or t=end. Always grab both.
|
|
108
|
+
ffmpeg -y -loglevel error -i "$VIDEO" -vf "$SCALE" -frames:v 1 -q:v 3 \
|
|
109
|
+
"$OUT/frames/anchor_first.jpg" || true
|
|
110
|
+
ffmpeg -y -loglevel error -sseof -0.5 -i "$VIDEO" -vf "$SCALE" -update 1 -frames:v 1 -q:v 3 \
|
|
111
|
+
"$OUT/frames/anchor_last.jpg" || true
|
|
112
|
+
|
|
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"
|
|
128
|
+
|
|
129
|
+
echo "==> [$SLUG] building frames.json (timestamp index, dedup near-dups)"
|
|
130
|
+
python3 "$SCRIPT_DIR/build_frame_index.py" "$OUT" "$DURATION"
|
|
131
|
+
|
|
132
|
+
NFRAMES="$(ls -1 "$OUT/frames"/*.jpg 2>/dev/null | wc -l | tr -d ' ')"
|
|
133
|
+
echo "==> [$SLUG] DONE"
|
|
134
|
+
echo " transcript: $OUT/transcript.srt"
|
|
135
|
+
echo " frames: $NFRAMES (index: $OUT/frames.json)"
|
|
136
|
+
echo " next: Claude reads transcript + frames.json, views key frames, writes the curated transcript.md"
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dreamcontext",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "dreamcontext — the persistent brain for your AI agents. Remembers what you built, knows how your project works.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
"agents",
|
|
14
14
|
"README.md",
|
|
15
15
|
"LICENSE",
|
|
16
|
+
"NOTICE",
|
|
16
17
|
"install.sh"
|
|
17
18
|
],
|
|
18
19
|
"scripts": {
|
|
@@ -23,6 +24,7 @@
|
|
|
23
24
|
"dev:dashboard": "cd dashboard && npm run dev",
|
|
24
25
|
"test": "vitest",
|
|
25
26
|
"test:e2e": "playwright test",
|
|
27
|
+
"diagrams": "node scripts/diagrams/build-all.mjs",
|
|
26
28
|
"prepublishOnly": "npm run build"
|
|
27
29
|
},
|
|
28
30
|
"keywords": [
|
|
@@ -36,7 +38,7 @@
|
|
|
36
38
|
"gemini",
|
|
37
39
|
"llm"
|
|
38
40
|
],
|
|
39
|
-
"license": "
|
|
41
|
+
"license": "Apache-2.0",
|
|
40
42
|
"engines": {
|
|
41
43
|
"node": ">=18"
|
|
42
44
|
},
|
package/skill/SKILL.md
CHANGED
|
@@ -318,7 +318,9 @@ Sleep debt reminders are injected on every user message (via UserPromptSubmit ho
|
|
|
318
318
|
- user hint mentions knowledge or a feature
|
|
319
319
|
- When unsure, **over-fire** `sleep-product` — it no-ops cheaply.
|
|
320
320
|
- Pass each specialist a small text brief in its prompt: epoch, session IDs, active task slugs, planning version, signals relevant to that specialist, optional user hint. Do **not** include transcript content — specialists call `dreamcontext transcript distill <id>` themselves.
|
|
321
|
+
- **Consolidation discipline (remind both specialists in the brief):** prefer *updating/extending* an existing entity over creating a new one. `sleep-tasks` folds a smaller slice into the task that already covers it (broaden its title + insert sub-items) rather than forking a duplicate or a needless sub-task; `sleep-product` keeps similar verticals/brands/topics in the fewest knowledge files, splitting only on a sharp topical boundary that sharpens tags. Duplicate tasks and fragmented near-duplicate knowledge are the top consolidation failure modes — but genuinely separate concerns/topics still get their own task/file.
|
|
321
322
|
5. Wait for all specialist reports. Each returns a short structured report.
|
|
323
|
+
5a. Run `dreamcontext reflect` — each candidate is a term seen across multiple sessions not yet in soul/user/memory/knowledge; promote into `2.memory.md` or a knowledge file ONLY if genuinely load-bearing; most are noise, discard; NEVER auto-promote.
|
|
322
324
|
6. Marketing pass if `_dream_context/marketing/` exists: `dreamcontext mk rem-sleep`.
|
|
323
325
|
7. Council promote check: `dreamcontext council list --unpromoted` — promote if user engaged positively.
|
|
324
326
|
8. `dreamcontext sleep done "<one-paragraph summary stitched from specialist reports>"` — clears pre-epoch state, resets debt.
|
|
@@ -505,6 +507,7 @@ All commands prefixed with `dreamcontext`. For reading/searching, use native too
|
|
|
505
507
|
| `trigger add "<when>" "<remind>" [-m max_fires] [-s source]` | Create contextual trigger |
|
|
506
508
|
| `trigger list` | Show active triggers |
|
|
507
509
|
| `trigger remove <id>` | Remove a trigger |
|
|
510
|
+
| `reflect [--min-sessions N] [--max N] [--write]` | Detect recurring cross-session terms not yet in knowledge/memory as CANDIDATES; `--write` persists to `state/.reflection.md` |
|
|
508
511
|
| `transcript distill <session_id>` | Extract high-signal content from session transcript |
|
|
509
512
|
| `sleep status` | Show sleep state and history |
|
|
510
513
|
| `sleep add <score> "<desc>"` | Manual debt add |
|
|
@@ -523,6 +526,8 @@ All commands prefixed with `dreamcontext`. For reading/searching, use native too
|
|
|
523
526
|
| `snapshot --tokens` | Estimate snapshot token count |
|
|
524
527
|
| `doctor` | Validate `_dream_context/` structure |
|
|
525
528
|
| `install-skill` | Install skill + hooks |
|
|
529
|
+
| `config show` | Print project config (platforms, packs, native-memory state) |
|
|
530
|
+
| `config native-memory <enable\|disable>` | Toggle Claude's native auto-memory; disabled by default so dreamcontext owns project memory |
|
|
526
531
|
| `upgrade [--check]` | Update the dreamcontext CLI itself to the latest npm release |
|
|
527
532
|
|
|
528
533
|
Feature insert sections: `changelog`, `notes`, `technical_details`, `constraints`, `user_stories`, `acceptance_criteria`, `why`
|
package/skill-packs/catalog.json
CHANGED
|
@@ -352,6 +352,50 @@
|
|
|
352
352
|
"security"
|
|
353
353
|
],
|
|
354
354
|
"alwaysApply": false
|
|
355
|
+
},
|
|
356
|
+
{
|
|
357
|
+
"name": "excalidraw",
|
|
358
|
+
"file": "excalidraw/SKILL.md",
|
|
359
|
+
"description": "Generate or extend Obsidian Excalidraw (.excalidraw.md) boards — lay out images/screenshots, text, shapes, arrows, frames, lanes and grids — by writing a small JSON spec and running a deterministic script that emits valid plugin markup (near-zero tokens, always renders). Embeds local images via the plugin's sha1 wikilink trick (no base64). Triggers: 'draw this in excalidraw', 'make an excalidraw board', 'excalidraw diagram', 'funnel map board', 'embed screenshots into excalidraw', 'excalidraw çiz', 'excalidraw board oluştur'.",
|
|
360
|
+
"tags": [
|
|
361
|
+
"excalidraw",
|
|
362
|
+
"obsidian",
|
|
363
|
+
"diagram",
|
|
364
|
+
"board",
|
|
365
|
+
"visualization",
|
|
366
|
+
"screenshots",
|
|
367
|
+
"funnel-map",
|
|
368
|
+
"drawing"
|
|
369
|
+
],
|
|
370
|
+
"alwaysApply": false,
|
|
371
|
+
"bundleDir": true
|
|
372
|
+
},
|
|
373
|
+
{
|
|
374
|
+
"name": "video-watching",
|
|
375
|
+
"file": "video-watching/SKILL.md",
|
|
376
|
+
"description": "Load when the user hands over a video file/link or asks to watch/analyze a video, extract a transcript, or 'bu videoda ne var'. Turns a video into a time-mapped transcript with on-screen visuals (slides, UI, charts, text) described inline, then reasons about it. Triggers: 'watch this video', 'videoyu izle/analiz et', 'transkript çıkar', 'video transcript'.",
|
|
377
|
+
"tags": [
|
|
378
|
+
"video",
|
|
379
|
+
"transcript",
|
|
380
|
+
"whisper",
|
|
381
|
+
"ffmpeg",
|
|
382
|
+
"frames",
|
|
383
|
+
"ocr",
|
|
384
|
+
"marketing-teardown",
|
|
385
|
+
"media"
|
|
386
|
+
],
|
|
387
|
+
"alwaysApply": false,
|
|
388
|
+
"bundleDir": true,
|
|
389
|
+
"requiresBinaries": [
|
|
390
|
+
"whisper-cpp",
|
|
391
|
+
"ffmpeg",
|
|
392
|
+
"yt-dlp (remote links only)"
|
|
393
|
+
],
|
|
394
|
+
"crossPackDeps": [
|
|
395
|
+
"growth (marketing-video teardown handoff)",
|
|
396
|
+
"meta-marketing (ad-creative teardown handoff)",
|
|
397
|
+
"design/onboarding-design (app-demo / FTE reconstruction handoff)"
|
|
398
|
+
]
|
|
355
399
|
}
|
|
356
400
|
],
|
|
357
401
|
"agents": [
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: excalidraw
|
|
3
|
+
description: >-
|
|
4
|
+
Generate or extend Obsidian Excalidraw (.excalidraw.md) boards in this vault — lay out
|
|
5
|
+
images/screenshots, text labels, shapes, arrows, frames, lanes and grids — by writing a small
|
|
6
|
+
JSON spec and letting a deterministic script emit valid plugin markup (so it costs ~no tokens and
|
|
7
|
+
always renders). Embeds local images via the plugin's sha1 wikilink trick (no base64). Triggers:
|
|
8
|
+
'/excalidraw', 'draw this in excalidraw', 'make an excalidraw board', 'excalidraw diagram',
|
|
9
|
+
'funnel map board', 'embed screenshots into excalidraw', 'add images to an excalidraw file',
|
|
10
|
+
'excalidraw çiz', 'excalidraw board oluştur', 'ekran görüntülerini excalidraw a ekle'.
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
# Excalidraw board generator (Obsidian)
|
|
14
|
+
|
|
15
|
+
Write `.excalidraw.md` files that render natively in this vault's Obsidian Excalidraw plugin.
|
|
16
|
+
**Do not hand-author the scene JSON.** Build a small spec and run the generator — it produces the
|
|
17
|
+
frontmatter, `## Text Elements`, `## Embedded Files`, and the `%% ## Drawing … %%` JSON, with
|
|
18
|
+
correct sha1 image links, fractional z-indices, and deterministic seeds (clean git diffs).
|
|
19
|
+
|
|
20
|
+
## The one trick that makes images work
|
|
21
|
+
An image element references `fileId` = **sha1 of the image file**. The `## Embedded Files` section
|
|
22
|
+
maps `<sha1>: [[image.png]]`. The plugin resolves the picture from the vault via that wikilink —
|
|
23
|
+
**no base64 needed.** The generator computes the sha1 and writes both sides for you.
|
|
24
|
+
|
|
25
|
+
## Usage
|
|
26
|
+
|
|
27
|
+
### CLI (most common)
|
|
28
|
+
```bash
|
|
29
|
+
node .claude/skills/excalidraw/scripts/build_excalidraw.js <spec.json> [--out <path.excalidraw.md>]
|
|
30
|
+
```
|
|
31
|
+
Write a spec JSON, then run it. The script prints `elements/images/texts` counts on success.
|
|
32
|
+
|
|
33
|
+
### JS API (for pipelines that generate many boards)
|
|
34
|
+
```js
|
|
35
|
+
const { buildExcalidraw, lane, grid } = require('.../scripts/build_excalidraw.js');
|
|
36
|
+
buildExcalidraw({ out, elements: [ ...lane({ title, images, x, y, thumbW }) ] });
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Spec schema
|
|
40
|
+
```jsonc
|
|
41
|
+
{
|
|
42
|
+
"out": "/abs/path/Board.excalidraw.md", // required (or pass --out)
|
|
43
|
+
"vaultRoot": "/abs/vault", // optional; auto-detected by walking up to `.obsidian`
|
|
44
|
+
"attachDir": "Attachments", // external images get copied here (relative to board dir)
|
|
45
|
+
"wikilinkMode": "basename", // "basename" (default) or "path" (vault-relative)
|
|
46
|
+
"background": "#ffffff",
|
|
47
|
+
"elements": [ /* ElementSpec… */ ]
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### Element types
|
|
52
|
+
- `text` — `{ x, y, text, fontSize?, color?, width?, align?, fontFamily? }` (fontFamily 1=hand, 2=normal, 3=code). **Set `width` for any caption/label that must stay inside a column or card** → the text WRAPS to that width (autoResize off) and its height is computed from the wrapped line count. Omit `width` only for short single-line text you want sized to content (it renders on one line and will overlap neighbours if long).
|
|
53
|
+
- `image` — `{ x, y, path, width? , height? }` — give ONE of width/height; the other is derived from aspect. `path` is an absolute file path.
|
|
54
|
+
- `rectangle`/`ellipse`/`diamond` — `{ x, y, width, height, strokeColor?, backgroundColor?, fillStyle?(solid|hachure|cross-hatch), strokeWidth?, roundness?(true=rounded) }`
|
|
55
|
+
- `line`/`arrow` — `{ points: [[x1,y1],[x2,y2],…], strokeColor?, strokeWidth?, endArrow?, startArrow? }` (coords are absolute; arrows default to an end arrowhead)
|
|
56
|
+
- `frame` — `{ x, y, width, height, name }`
|
|
57
|
+
|
|
58
|
+
### Layout helpers (JS API) — dimension-aware, never gap/overlap
|
|
59
|
+
Both read each image's real pixel size at layout time, so every caption **hugs its own image's bottom**
|
|
60
|
+
(no fixed-aspect guess → no big gap under a short screenshot, no overlap under a tall one). They return
|
|
61
|
+
an `ElementSpec[]` you spread with `...`, and that array also carries `.height`, `.width`, and **`.nextY`**
|
|
62
|
+
so you can stack lanes/grids/sections without doing height math by hand.
|
|
63
|
+
- `lane({ title, images:[{path, caption}], x, y, thumbW, gap, captionSize, captionGap })` → titled horizontal
|
|
64
|
+
strip; caption wraps to `thumbW` under each thumb. Ideal for **funnel step maps** (one lane per funnel).
|
|
65
|
+
- `grid({ images:[{path, caption}], x, y, cols, thumbW, gapX, gapY, captionSize })` → wrapping grid; each
|
|
66
|
+
row sizes to its tallest image, each caption hugs its image.
|
|
67
|
+
- `thumbHeight(path, thumbW)` → the on-board height a thumbnail of width `thumbW` will get (exported for
|
|
68
|
+
custom layouts that need to place captions/rows themselves).
|
|
69
|
+
```js
|
|
70
|
+
const a = lane({ title:'Pilates', images, x:60, y:120, thumbW:150 });
|
|
71
|
+
const b = lane({ title:'Fasting', images2, x:60, y:a.nextY, thumbW:150 }); // stacks under `a`, no overlap
|
|
72
|
+
buildExcalidraw({ out, elements:[ ...a, ...b ] });
|
|
73
|
+
```
|
|
74
|
+
House-style `card()` labels are also vertically centered correctly even when the label **wraps** to the card width.
|
|
75
|
+
|
|
76
|
+
## House style (`scripts/lib/style.js`) — learned from the vault's presentation boards
|
|
77
|
+
Match the team's look by default. The builder now defaults text to **Excalifont (fontFamily 5)**.
|
|
78
|
+
`style.js` exports the palette + ready-made builders so boards look hand-drawn and consistent:
|
|
79
|
+
|
|
80
|
+
- **`PALETTE`** (semantic, = Excalidraw's native swatches): `green` benefit/go · `red` pain/risk ·
|
|
81
|
+
`blue` system/flow · `purple` core service · `yellow` processing · `mint` result · `gray` muted.
|
|
82
|
+
Each is `{ fill, stroke }`. `INK` = `#1e1e1e` (the default 2px card outline). Fills are **solid**,
|
|
83
|
+
rects are **always rounded**.
|
|
84
|
+
- **`card({x,y,w,h,text,color,fontSize})`** → rounded filled rect + centered label, grouped (move as one).
|
|
85
|
+
- **`node(...)`** → smaller card for flow steps.
|
|
86
|
+
- **`sectionTitle({x,y,text,fontSize})`** → big plain header (no box), like "Configuration" / "Flow".
|
|
87
|
+
- **`connector({from,to,label,double,dashed})`** → labeled arrow between two points.
|
|
88
|
+
- **`annotate({from,to,text})`** → short arrow to a side-spec note (the "90sec / deepseek…" pattern).
|
|
89
|
+
- **`column({x,y,items,color})`** → vertical stack of cards (benefit/risk lists). `items` may be
|
|
90
|
+
strings or `{text,color}`.
|
|
91
|
+
- **`hub({cx,cy,label,spokes})`** → central node + satellites with arrows (the "Layer + APIs" pattern).
|
|
92
|
+
- **`bullets({x,y,items})`** → one left-aligned bulleted text block (their spec-list pattern).
|
|
93
|
+
- Edge helpers `center/rightOf/leftOf/topOf/bottomOf(x,y,w,h)` to wire connectors to card edges.
|
|
94
|
+
|
|
95
|
+
Compose these into a `buildExcalidraw({ out, elements })` call. Runnable example: `examples/style_board.js`.
|
|
96
|
+
Conventions: color nodes by ROLE, label every arrow, put specs in side-annotations, use big plain
|
|
97
|
+
section titles to break the board into "slides", keep one idea per card.
|
|
98
|
+
|
|
99
|
+
## Rules & gotchas
|
|
100
|
+
- **Unique image filenames.** Obsidian resolves `[[name.png]]` by basename; if two different files
|
|
101
|
+
share a name anywhere in the vault the link is ambiguous. Name screenshots uniquely
|
|
102
|
+
(e.g. `bm-pilates-2707-step-03.png`) or use `"wikilinkMode": "path"`.
|
|
103
|
+
- Images must live inside the vault. External paths are auto-copied into `attachDir`.
|
|
104
|
+
- Output is uncompressed JSON inside `%% … %%`. The plugin reads it; on its first save it may
|
|
105
|
+
re-compress — that's expected and harmless.
|
|
106
|
+
- Re-running with the same `out` + same spec yields a byte-stable file (seeds are derived from the
|
|
107
|
+
output path), so boards diff cleanly in git.
|
|
108
|
+
- **Self-contained / portable.** No `npm install` — `build_excalidraw.js` uses only Node builtins +
|
|
109
|
+
the vendored `scripts/lib/*` (fractional-indexing, imagesize, style). Copy `.claude/skills/excalidraw/`
|
|
110
|
+
into any project and it works. `vaultRoot` auto-detects `.obsidian`; with none it falls back to the
|
|
111
|
+
board's own folder, so the `.excalidraw.md` is still written (it just renders inside Obsidian).
|
|
112
|
+
|
|
113
|
+
## Verify
|
|
114
|
+
Run the bundled examples from the skill folder (they use skill-relative paths + a bundled `sample.png`,
|
|
115
|
+
so they work in any project):
|
|
116
|
+
```bash
|
|
117
|
+
cd .claude/skills/excalidraw
|
|
118
|
+
node scripts/build_excalidraw.js examples/hello.spec.json # spec-driven: text/shapes/image → examples/Hello Excalidraw.excalidraw.md
|
|
119
|
+
node examples/style_board.js # JS API: card/connector/lane house style → examples/Style Demo.excalidraw.md
|
|
120
|
+
```
|
|
121
|
+
Then open the resulting board in Obsidian (Excalidraw view) to confirm it renders.
|
|
122
|
+
|
|
123
|
+
See `reference/format.md` for the exact `.excalidraw.md` anatomy reverse-engineered from this vault.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"out": "examples/Hello Excalidraw.excalidraw.md",
|
|
3
|
+
"background": "#ffffff",
|
|
4
|
+
"elements": [
|
|
5
|
+
{ "type": "text", "x": 0, "y": -70, "text": "excalidraw skill — portable smoke test", "fontSize": 28, "color": "#1971c2" },
|
|
6
|
+
{ "type": "text", "x": 0, "y": -28, "text": "Give a text a width and it wraps to that width (no overlap). The image embeds via sha1 wikilink — no base64. Only skill-relative paths, so this runs in any project.", "fontSize": 15, "color": "#868e96", "width": 540 },
|
|
7
|
+
{ "type": "rectangle", "x": 0, "y": 40, "width": 260, "height": 140, "strokeColor": "#1e1e1e", "backgroundColor": "#a5d8ff", "fillStyle": "solid", "strokeWidth": 2, "roundness": true },
|
|
8
|
+
{ "type": "text", "x": 24, "y": 96, "text": "rounded rect\n+ fill", "fontSize": 20, "color": "#1e1e1e" },
|
|
9
|
+
{ "type": "ellipse", "x": 340, "y": 40, "width": 160, "height": 140, "strokeColor": "#2f9e44", "backgroundColor": "#b2f2bb", "fillStyle": "hachure" },
|
|
10
|
+
{ "type": "arrow", "points": [[260, 110], [340, 110]], "strokeColor": "#e8590c", "strokeWidth": 3 },
|
|
11
|
+
{ "type": "text", "x": 0, "y": 220, "text": "embedded image (resolved from vault by sha1 wikilink):", "fontSize": 16, "color": "#555", "width": 420 },
|
|
12
|
+
{ "type": "image", "x": 0, "y": 250, "path": "examples/sample.png", "width": 320 }
|
|
13
|
+
]
|
|
14
|
+
}
|
|
Binary file
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// style_board.js — house-style demo. Run from anywhere: node examples/style_board.js
|
|
2
|
+
// Shows card/connector/sectionTitle + the dimension-aware lane() (captions hug images, .nextY stacks).
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const { buildExcalidraw, lane } = require(path.join(__dirname, '..', 'scripts', 'build_excalidraw.js'));
|
|
5
|
+
const { card, connector, sectionTitle, rightOf, leftOf, bottomOf, topOf } = require(path.join(__dirname, '..', 'scripts', 'lib', 'style.js'));
|
|
6
|
+
|
|
7
|
+
const SAMPLE = path.join(__dirname, 'sample.png');
|
|
8
|
+
const els = [];
|
|
9
|
+
const P = (...a) => els.push(...a.flat());
|
|
10
|
+
|
|
11
|
+
// --- flow row with labeled connectors (color nodes by role) ---
|
|
12
|
+
P(sectionTitle({ x: 0, y: 0, text: 'House style', fontSize: 40 }));
|
|
13
|
+
const A = { x: 0, y: 90, w: 220, h: 84 }, B = { x: 360, y: 90, w: 220, h: 84 }, C = { x: 720, y: 90, w: 220, h: 84 };
|
|
14
|
+
P(card({ ...A, text: 'input', color: 'blue' }));
|
|
15
|
+
P(card({ ...B, text: 'core service', color: 'purple' }));
|
|
16
|
+
P(card({ ...C, text: 'result', color: 'mint' }));
|
|
17
|
+
P(connector({ from: rightOf(A.x, A.y, A.w, A.h), to: leftOf(B.x, B.y, B.w, B.h), label: 'request' }));
|
|
18
|
+
P(connector({ from: rightOf(B.x, B.y, B.w, B.h), to: leftOf(C.x, C.y, C.w, C.h), label: 'response' }));
|
|
19
|
+
|
|
20
|
+
// --- a dimension-aware lane: captions hug each image, .nextY gives the next free row ---
|
|
21
|
+
const imgs = [
|
|
22
|
+
{ path: SAMPLE, caption: 'lane() reads the real image size, so this caption hugs the bottom and wraps to the thumb width — no gap, no overlap.' },
|
|
23
|
+
{ path: SAMPLE, caption: 'second thumb' },
|
|
24
|
+
{ path: SAMPLE, caption: 'third thumb' },
|
|
25
|
+
];
|
|
26
|
+
const strip = lane({ title: 'lane() — funnel-step style strip', images: imgs, x: 0, y: 220, thumbW: 200 });
|
|
27
|
+
P(strip);
|
|
28
|
+
P(sectionTitle({ x: 0, y: strip.nextY, text: 'stacked below via strip.nextY', fontSize: 22, color: '#868e96' }));
|
|
29
|
+
|
|
30
|
+
buildExcalidraw({ out: path.join(__dirname, 'Style Demo.excalidraw.md'), elements: els, background: '#fbfbfb' });
|
|
31
|
+
console.log('style demo →', path.join(__dirname, 'Style Demo.excalidraw.md'));
|