From a4f4e08ee4dbb8c267fe2c8025bafb467056ca75 Mon Sep 17 00:00:00 2001 From: Jake Shore Date: Mon, 26 Jan 2026 16:14:58 -0500 Subject: [PATCH] Add PageIndex-style memory system + Genre Universe documentation --- .clawdhub/lock.json | 8 + .gitignore | 1 + AGENTS.md | 10 +- USER.md | 12 + genre-viz/analyzer/analyze_album.py | 176 ++++ genre-viz/analyzer/analyze_track.py | 157 ++++ genre-viz/index.html | 597 +++++++++++++ genre-viz/start-genre-universe.sh | 5 + genre-viz/vercel.json | 10 + memory/2026-01-26-genre-universe.md | 90 ++ memory/2026-01-26.md | 79 ++ memory/INDEX.json | 185 ++++ memory/burton-method-research-intel.md | 90 ++ reonomy-auth.json | 193 +++++ reonomy-demo-video/STORYBOARD.md | 322 +++++++ reonomy-demo-video/package.json | 20 + reonomy-demo-video/remotion.config.ts | 4 + reonomy-demo-video/src/ReonomyCanvasDemo.tsx | 576 ++++++++++++ reonomy-demo-video/src/ReonomyDemo.tsx | 635 ++++++++++++++ .../src/ReonomyDemoExciting.tsx | 819 ++++++++++++++++++ reonomy-demo-video/src/Root.tsx | 35 + reonomy-demo-video/src/index.ts | 4 + reonomy-demo-video/tsconfig.json | 17 + reonomy-demo.sh | 124 +++ reonomy-leads-v13.json | 6 + reonomy-quick-demo.js | 125 +++ reonomy-scraper-v13.js | 442 ++++++++++ skills/agent-browser/.clawdhub/origin.json | 7 + skills/agent-browser/CONTRIBUTING.md | 63 ++ skills/agent-browser/SKILL.md | 328 +++++++ skills/browser-use/.clawdhub/origin.json | 7 + skills/browser-use/SKILL.md | 162 ++++ 32 files changed, 5308 insertions(+), 1 deletion(-) create mode 100644 genre-viz/analyzer/analyze_album.py create mode 100644 genre-viz/analyzer/analyze_track.py create mode 100644 genre-viz/index.html create mode 100755 genre-viz/start-genre-universe.sh create mode 100644 genre-viz/vercel.json create mode 100644 memory/2026-01-26-genre-universe.md create mode 100644 memory/2026-01-26.md create mode 100644 memory/INDEX.json create mode 100644 memory/burton-method-research-intel.md create mode 100644 reonomy-auth.json create mode 100644 reonomy-demo-video/STORYBOARD.md create mode 100644 reonomy-demo-video/package.json create mode 100644 reonomy-demo-video/remotion.config.ts create mode 100644 reonomy-demo-video/src/ReonomyCanvasDemo.tsx create mode 100644 reonomy-demo-video/src/ReonomyDemo.tsx create mode 100644 reonomy-demo-video/src/ReonomyDemoExciting.tsx create mode 100644 reonomy-demo-video/src/Root.tsx create mode 100644 reonomy-demo-video/src/index.ts create mode 100644 reonomy-demo-video/tsconfig.json create mode 100755 reonomy-demo.sh create mode 100644 reonomy-leads-v13.json create mode 100755 reonomy-quick-demo.js create mode 100755 reonomy-scraper-v13.js create mode 100644 skills/agent-browser/.clawdhub/origin.json create mode 100644 skills/agent-browser/CONTRIBUTING.md create mode 100644 skills/agent-browser/SKILL.md create mode 100644 skills/browser-use/.clawdhub/origin.json create mode 100644 skills/browser-use/SKILL.md diff --git a/.clawdhub/lock.json b/.clawdhub/lock.json index 0081024..a7b1536 100644 --- a/.clawdhub/lock.json +++ b/.clawdhub/lock.json @@ -8,6 +8,14 @@ "frontend-design": { "version": "1.0.0", "installedAt": 1769325240066 + }, + "browser-use": { + "version": "1.0.0", + "installedAt": 1769422454571 + }, + "agent-browser": { + "version": "0.2.0", + "installedAt": 1769423250734 } } } diff --git a/.gitignore b/.gitignore index 6f860dd..12030cb 100644 --- a/.gitignore +++ b/.gitignore @@ -26,3 +26,4 @@ npm-debug.log* package-lock.json reonomy-scraper.log Thumbs.db +pageindex-framework/ diff --git a/AGENTS.md b/AGENTS.md index 35e1466..f51783b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -39,4 +39,12 @@ This keeps identity, memory, and progress backed up. Consider making it private - Add your preferred style, rules, and "memory" here. ## Discord-specific rule -- If you ever feel like you lack context in a Discord conversation, **proactively read the past few messages** in that channel using the message tool (action=search or action=read with before/after parameters) before asking for clarification. \ No newline at end of file +- If you ever feel like you lack context in a Discord conversation, **proactively read the past few messages** in that channel using the message tool (action=search or action=read with before/after parameters) before asking for clarification. + +## Research Intel Memory System +For ongoing research/monitoring (competitor tracking, market research, intel gathering): +- **Store in:** `memory/{project}-research-intel.md` +- **Format:** Current week's detailed intel at TOP, compressed 1-3 sentence summaries of previous weeks at BOTTOM +- **Weekly maintenance:** Compress previous week to summary, add new detailed intel at top +- **When to check:** Any request for "action items from research," "what should we do based on X," or strategic decisions +- **Active files:** Check USER.md for list of active research intel files \ No newline at end of file diff --git a/USER.md b/USER.md index e8ce8f7..8fd93e2 100644 --- a/USER.md +++ b/USER.md @@ -28,6 +28,7 @@ - **LSAT edtech company ("The Burton Method")** - Tutoring + drilling platform with community, recurring revenue, and plans for **AI-driven customization**. - Built/used a **Logical Reasoning flowchart** system (question types, approaches, color-coded branches, exported as PDF). + - **Research intel:** `memory/burton-method-research-intel.md` โ€” weekly competitor + EdTech digest with action items - **Real estate / CRE CRM + onboarding automation ("CRE Sync CRM", "Real Connect V2")** - Designing a **conditional onboarding flow** that routes users based on goals, lead sources, CRM usage, brokerage/GCI, recruiting/coaching, etc. @@ -69,6 +70,17 @@ - **Git backup**: Run `cd ~/.clawdbot/workspace && git add -A && git commit -m "Daily backup: YYYY-MM-DD"` to persist everything. - **Context refresh**: On session start, read today + yesterday's memory files. +### Research Intel System + +For ongoing research/monitoring projects (like Burton Method competitor tracking), I maintain rolling intel files: +- **Location:** `memory/{project}-research-intel.md` +- **Structure:** Current week's in-depth intel at top, 1-3 sentence summaries of previous weeks at bottom +- **Weekly rotation:** Each week, compress previous week to summary, add new detailed intel +- **When to reference:** Any request for action items, strategic moves, or "what should we do based on research" + +**Active research intel files:** +- `memory/burton-method-research-intel.md` โ€” Competitor + EdTech trends for The Burton Method + ### Who you are (based on what youโ€™ve shared) - **Jake** โ€” a builder/operator who runs multiple tracks at once: edtech, real estate/CRM tooling, CFO-style business strategy, and creative projects. diff --git a/genre-viz/analyzer/analyze_album.py b/genre-viz/analyzer/analyze_album.py new file mode 100644 index 0000000..4fb3b98 --- /dev/null +++ b/genre-viz/analyzer/analyze_album.py @@ -0,0 +1,176 @@ +# /// script +# requires-python = ">=3.10" +# dependencies = [ +# "librosa>=0.10.0", +# "numpy", +# "scipy", +# "soundfile", +# ] +# /// + +import librosa +import numpy as np +import sys +import json +import os +from pathlib import Path + +def analyze_track(audio_path): + """Analyze a single track and return features.""" + print(f" Analyzing: {os.path.basename(audio_path)}") + + y, sr = librosa.load(audio_path, sr=22050, duration=180) + + # Tempo + tempo, _ = librosa.beat.beat_track(y=y, sr=sr) + tempo = float(tempo) if not hasattr(tempo, '__len__') else float(tempo[0]) + + # Energy + rms = librosa.feature.rms(y=y)[0] + energy = float(np.mean(rms)) + energy_std = float(np.std(rms)) + + # Spectral features + spectral_centroid = librosa.feature.spectral_centroid(y=y, sr=sr)[0] + brightness = float(np.mean(spectral_centroid)) + + # Harmonic/Percussive + y_harmonic, y_percussive = librosa.effects.hpss(y) + harmonic_ratio = float(np.sum(np.abs(y_harmonic)) / (np.sum(np.abs(y)) + 1e-6)) + + # Chroma + chroma = librosa.feature.chroma_stft(y=y, sr=sr) + chroma_mean = float(np.mean(chroma)) + + # Onset strength + onset_env = librosa.onset.onset_strength(y=y, sr=sr) + rhythmic_density = float(np.mean(onset_env)) + + # Zero crossing rate + zcr = librosa.feature.zero_crossing_rate(y)[0] + percussiveness = float(np.mean(zcr)) + + return { + "tempo_bpm": tempo, + "energy_rms": energy, + "energy_std": energy_std, + "brightness_hz": brightness, + "harmonic_ratio": harmonic_ratio, + "chroma_mean": chroma_mean, + "rhythmic_density": rhythmic_density, + "percussiveness": percussiveness, + } + +def normalize_features(raw): + """Convert raw features to 0-1 Genre Universe scales.""" + # Tempo: 60-180 BPM + tempo_norm = np.clip((raw["tempo_bpm"] - 60) / 120, 0, 1) + + # Energy: RMS 0.01-0.25 + energy_norm = np.clip(raw["energy_rms"] / 0.2, 0, 1) + + # Brightness: 1000-4000 Hz + brightness_norm = np.clip((raw["brightness_hz"] - 1000) / 3000, 0, 1) + + # Organic score + organic_norm = np.clip(raw["harmonic_ratio"] * 1.2 - brightness_norm * 0.2, 0, 1) + + # Valence estimate (rough) + valence_norm = np.clip(0.25 + brightness_norm * 0.25 + raw["chroma_mean"] * 0.3, 0, 1) + + # Danceability + dance_tempo = 1 - abs(raw["tempo_bpm"] - 120) / 60 + danceability = np.clip(dance_tempo * 0.4 + raw["rhythmic_density"] * 0.35 + energy_norm * 0.25, 0, 1) + + # Acousticness (related to organic but different) + acousticness = np.clip(raw["harmonic_ratio"] * 0.7 + (1 - brightness_norm) * 0.3, 0, 1) + + # Production density + prod_density = np.clip((1 - raw["harmonic_ratio"]) * 0.5 + raw["energy_std"] * 8 + energy_norm * 0.3, 0, 1) + + return { + "position": { + "valence": round(float(valence_norm), 2), + "tempo": round(float(tempo_norm), 2), + "organic": round(float(organic_norm), 2), + }, + "spikes": { + "energy": round(float(energy_norm), 2), + "acousticness": round(float(acousticness), 2), + "danceability": round(float(danceability), 2), + "production_density": round(float(prod_density), 2), + } + } + +if __name__ == "__main__": + tracks_dir = Path("surya_tracks") + mp3_files = list(tracks_dir.glob("*.mp3")) + + if not mp3_files: + print("No MP3 files found!") + sys.exit(1) + + print(f"\n๐ŸŽต ANALYZING {len(mp3_files)} TRACKS FROM SURYA ALBUM\n") + print("="*60) + + all_raw = [] + track_results = [] + + for mp3 in sorted(mp3_files): + raw = analyze_track(str(mp3)) + all_raw.append(raw) + normalized = normalize_features(raw) + track_results.append({ + "track": mp3.stem, + "raw": raw, + "normalized": normalized + }) + print(f" Tempo: {raw['tempo_bpm']:.1f} BPM | Energy: {raw['energy_rms']:.3f} | Harmonic: {raw['harmonic_ratio']:.2f}") + + # Average all tracks + print("\n" + "="*60) + print("๐Ÿ“Š ALBUM AVERAGES") + print("="*60) + + avg_raw = { + key: np.mean([t[key] for t in all_raw]) + for key in all_raw[0].keys() + } + + print(f"\n Average Tempo: {avg_raw['tempo_bpm']:.1f} BPM") + print(f" Average Energy: {avg_raw['energy_rms']:.4f}") + print(f" Average Brightness: {avg_raw['brightness_hz']:.1f} Hz") + print(f" Average Harmonic Ratio: {avg_raw['harmonic_ratio']:.3f}") + + avg_normalized = normalize_features(avg_raw) + + print("\n" + "="*60) + print("๐ŸŽฏ RECOMMENDED DAS POSITION (Album Average)") + print("="*60) + + pos = avg_normalized["position"] + spikes = avg_normalized["spikes"] + + print(f"\n Position in Genre Universe:") + print(f" valence: {pos['valence']} (mood: {'sad' if pos['valence'] < 0.4 else 'happy' if pos['valence'] > 0.6 else 'bittersweet'})") + print(f" tempo: {pos['tempo']} ({'slow' if pos['tempo'] < 0.4 else 'fast' if pos['tempo'] > 0.6 else 'medium'})") + print(f" organic: {pos['organic']} ({'electronic' if pos['organic'] < 0.4 else 'organic' if pos['organic'] > 0.6 else 'balanced'})") + + print(f"\n Spike Values:") + for key, val in spikes.items(): + bar = "โ–ˆ" * int(val * 20) + "โ–‘" * (20 - int(val * 20)) + print(f" {key:20} [{bar}] {val}") + + # Output JSON for programmatic use + result = { + "tracks_analyzed": len(mp3_files), + "track_names": [mp3.stem for mp3 in sorted(mp3_files)], + "raw_averages": {k: round(v, 4) for k, v in avg_raw.items()}, + "recommended_position": pos, + "recommended_spikes": spikes, + } + + print("\n" + "="*60) + print("๐Ÿ“„ JSON OUTPUT") + print("="*60) + print(json.dumps(result, indent=2)) diff --git a/genre-viz/analyzer/analyze_track.py b/genre-viz/analyzer/analyze_track.py new file mode 100644 index 0000000..1d7afdb --- /dev/null +++ b/genre-viz/analyzer/analyze_track.py @@ -0,0 +1,157 @@ +# /// script +# requires-python = ">=3.10" +# dependencies = [ +# "librosa>=0.10.0", +# "numpy", +# "scipy", +# "soundfile", +# ] +# /// + +import librosa +import numpy as np +import sys +import json + +def analyze_track(audio_path): + """ + Analyze an audio track and extract features for Genre Universe positioning. + """ + print(f"Loading: {audio_path}") + + # Load the audio file + y, sr = librosa.load(audio_path, sr=22050, duration=180) # First 3 minutes + + print("Analyzing audio features...") + + # === TEMPO / RHYTHM === + tempo, beat_frames = librosa.beat.beat_track(y=y, sr=sr) + tempo = float(tempo) if not hasattr(tempo, '__len__') else float(tempo[0]) + + # === ENERGY === + rms = librosa.feature.rms(y=y)[0] + energy = float(np.mean(rms)) + energy_std = float(np.std(rms)) # Dynamic range indicator + + # === SPECTRAL FEATURES === + spectral_centroid = librosa.feature.spectral_centroid(y=y, sr=sr)[0] + brightness = float(np.mean(spectral_centroid)) # Higher = brighter/more electronic + + spectral_rolloff = librosa.feature.spectral_rolloff(y=y, sr=sr)[0] + rolloff = float(np.mean(spectral_rolloff)) + + spectral_contrast = librosa.feature.spectral_contrast(y=y, sr=sr) + contrast = float(np.mean(spectral_contrast)) + + # === ZERO CROSSING RATE (percussiveness) === + zcr = librosa.feature.zero_crossing_rate(y)[0] + percussiveness = float(np.mean(zcr)) + + # === MFCC (timbral texture) === + mfccs = librosa.feature.mfcc(y=y, sr=sr, n_mfcc=13) + mfcc_mean = [float(np.mean(mfcc)) for mfcc in mfccs] + + # === CHROMA (harmonic content) === + chroma = librosa.feature.chroma_stft(y=y, sr=sr) + chroma_mean = float(np.mean(chroma)) + + # === ONSET STRENGTH (rhythmic density) === + onset_env = librosa.onset.onset_strength(y=y, sr=sr) + rhythmic_density = float(np.mean(onset_env)) + + # === HARMONIC/PERCUSSIVE SEPARATION === + y_harmonic, y_percussive = librosa.effects.hpss(y) + harmonic_ratio = float(np.sum(np.abs(y_harmonic)) / (np.sum(np.abs(y)) + 1e-6)) + + # === NORMALIZE TO 0-1 SCALES === + # These normalizations are rough estimates based on typical ranges + + # Tempo: 60-180 BPM typical range + tempo_normalized = np.clip((tempo - 60) / 120, 0, 1) + + # Energy: RMS typically 0.01-0.3 + energy_normalized = np.clip(energy / 0.2, 0, 1) + + # Brightness: spectral centroid typically 1000-4000 Hz + brightness_normalized = np.clip((brightness - 1000) / 3000, 0, 1) + + # Organic vs Electronic (inverse of brightness + harmonic ratio) + organic_score = np.clip(harmonic_ratio * 1.5 - brightness_normalized * 0.3, 0, 1) + + # Valence estimation (very rough - higher brightness + major key tendencies = happier) + # This is a simplification - real valence detection is complex + valence_estimate = np.clip(0.3 + brightness_normalized * 0.3 + chroma_mean * 0.2, 0, 1) + + # Danceability (tempo in sweet spot + strong beats + rhythmic density) + dance_tempo_factor = 1 - abs(tempo - 120) / 60 # Peak at 120 BPM + danceability = np.clip(dance_tempo_factor * 0.5 + rhythmic_density * 0.3 + energy_normalized * 0.2, 0, 1) + + results = { + "raw_features": { + "tempo_bpm": round(tempo, 1), + "energy_rms": round(energy, 4), + "energy_std": round(energy_std, 4), + "brightness_hz": round(brightness, 1), + "spectral_rolloff": round(rolloff, 1), + "spectral_contrast": round(contrast, 2), + "percussiveness": round(percussiveness, 4), + "rhythmic_density": round(rhythmic_density, 2), + "harmonic_ratio": round(harmonic_ratio, 3), + "chroma_mean": round(chroma_mean, 3), + }, + "genre_universe_position": { + "valence": round(float(valence_estimate), 2), + "tempo": round(float(tempo_normalized), 2), + "organic": round(float(organic_score), 2), + }, + "genre_universe_spikes": { + "energy": round(float(energy_normalized), 2), + "acousticness": round(float(organic_score * 0.8), 2), + "danceability": round(float(danceability), 2), + "production_density": round(float(1 - harmonic_ratio + energy_std * 5), 2), + }, + "insights": { + "tempo_feel": "slow" if tempo < 90 else "medium" if tempo < 130 else "fast", + "energy_level": "low" if energy_normalized < 0.33 else "medium" if energy_normalized < 0.66 else "high", + "sonic_character": "organic/warm" if organic_score > 0.6 else "electronic/bright" if organic_score < 0.4 else "balanced", + } + } + + return results + +if __name__ == "__main__": + if len(sys.argv) < 2: + print("Usage: python analyze_track.py ") + sys.exit(1) + + audio_path = sys.argv[1] + results = analyze_track(audio_path) + + print("\n" + "="*60) + print("GENRE UNIVERSE AUDIO ANALYSIS") + print("="*60) + + print("\n๐Ÿ“Š RAW AUDIO FEATURES:") + for key, value in results["raw_features"].items(): + print(f" {key}: {value}") + + print("\n๐ŸŽฏ GENRE UNIVERSE POSITION (0-1 scale):") + pos = results["genre_universe_position"] + print(f" X (Valence/Mood): {pos['valence']} {'โ† sad' if pos['valence'] < 0.4 else 'โ†’ happy' if pos['valence'] > 0.6 else '~ neutral'}") + print(f" Y (Tempo): {pos['tempo']} {'โ† slow' if pos['tempo'] < 0.4 else 'โ†’ fast' if pos['tempo'] > 0.6 else '~ medium'}") + print(f" Z (Organic): {pos['organic']} {'โ† electronic' if pos['organic'] < 0.4 else 'โ†’ organic' if pos['organic'] > 0.6 else '~ balanced'}") + + print("\nโšก SPIKE VALUES (0-1 scale):") + for key, value in results["genre_universe_spikes"].items(): + bar = "โ–ˆ" * int(value * 20) + "โ–‘" * (20 - int(value * 20)) + print(f" {key:20} [{bar}] {value}") + + print("\n๐Ÿ’ก INSIGHTS:") + for key, value in results["insights"].items(): + print(f" {key}: {value}") + + print("\n" + "="*60) + + # Also output JSON for programmatic use + print("\n๐Ÿ“„ JSON OUTPUT:") + print(json.dumps(results, indent=2)) diff --git a/genre-viz/index.html b/genre-viz/index.html new file mode 100644 index 0000000..ba6fc9f --- /dev/null +++ b/genre-viz/index.html @@ -0,0 +1,597 @@ + + + + + + Genre Universe - Das Artist Positioning + + + + +
+ + +
+

๐ŸŽต Genre Universe

+

Interactive 3D visualization of musical genre space. Artists positioned by sonic DNA, spikes show dimensional attributes.

+

DRAG โ†’ ROTATE โ€ข SCROLL โ†’ ZOOM โ€ข HOVER โ†’ INFO

+
+ +
+

Artists

+
+
+ +
+

Dimensions

+
โ†‘ Energy
+
โ†“ Acousticness
+
โ† Emotion
+
โ†’ Danceability
+

Position

+
X: Valence
+
Y: Tempo
+
Z: Organic
+
+ +
+

Controls

+
+
+
+
+
+
+ + +
+ +
+

+

+
+
+ + + + + + diff --git a/genre-viz/start-genre-universe.sh b/genre-viz/start-genre-universe.sh new file mode 100755 index 0000000..ea07b95 --- /dev/null +++ b/genre-viz/start-genre-universe.sh @@ -0,0 +1,5 @@ +#!/bin/bash +cd /Users/jakeshore/.clawdbot/workspace/genre-viz +/usr/bin/python3 -m http.server 8420 & +sleep 2 +/opt/homebrew/bin/cloudflared tunnel --url http://localhost:8420 diff --git a/genre-viz/vercel.json b/genre-viz/vercel.json new file mode 100644 index 0000000..c8c292d --- /dev/null +++ b/genre-viz/vercel.json @@ -0,0 +1,10 @@ +{ + "version": 2, + "name": "genre-universe", + "builds": [ + { "src": "index.html", "use": "@vercel/static" } + ], + "routes": [ + { "src": "/(.*)", "dest": "/index.html" } + ] +} diff --git a/memory/2026-01-26-genre-universe.md b/memory/2026-01-26-genre-universe.md new file mode 100644 index 0000000..21a98bf --- /dev/null +++ b/memory/2026-01-26-genre-universe.md @@ -0,0 +1,90 @@ +# Genre Universe - 3D Artist Positioning Visualization + +**Created:** 2026-01-26 +**Location:** `~/.clawdbot/workspace/genre-viz/` +**Live URL:** http://localhost:8420 (service running on Mac mini) + +## What It Is + +Interactive Three.js 3D visualization showing where Das sits in the genre landscape relative to other artists. Built to help with artist positioning, playlist pitching, and understanding "where does Das fit in the landscape" visually. + +## Technical Stack + +- **Frontend:** Vanilla HTML + Three.js (no build step) +- **Features:** OrbitControls, UnrealBloomPass post-processing, CSS2DRenderer for labels +- **Hosting:** Python http.server on port 8420, launchd service for auto-start + +## Dimensions + +### Position (3D axes) +- **X axis (left/right):** SAD โ† โ†’ HAPPY (valence) +- **Y axis (up/down):** SLOW โ† โ†’ FAST (tempo) +- **Z axis (front/back):** ELECTRONIC โ† โ†’ ORGANIC + +### Spike Extensions (6 directions per artist) +- โ†‘ Energy +- โ†“ Acousticness +- โ† Emotional Depth +- โ†’ Danceability +- โ†— Lyrical Complexity +- โ†™ Production Density + +## Das's Profile + +**Position:** +- Valence: 0.43 (slightly melancholy) +- Tempo: 0.35 (slower, breathing tracks) +- Organic: **0.70** (high - singer-songwriter core) + +**Spikes:** +- Emotional Depth: 0.95 (highest) +- Energy: 0.90 +- Danceability: 0.85 +- Lyric Complexity: 0.85 +- Acousticness: 0.60 +- Production Density: 0.80 + +## Artist Count + +**Peak:** 56 artists mapped (as of 09:49 UTC) +**Current:** 24 artists (accidentally reduced during visual updates) + +Artists included: Das, San Holo, Illenium, Seven Lions, Kygo, Joji, Porter Robinson, Odesza, Subtronics, Brakence, Flume, Frank Ocean, keshi, Eden, Clairo, KSHMR, Rezz, Excision, Marshmello, Madeon, Daniel Caesar, Bonobo, Virtual Self, Yung Lean + +## Files + +- `/Users/jakeshore/.clawdbot/workspace/genre-viz/index.html` - Main visualization +- `/Users/jakeshore/.clawdbot/workspace/genre-viz/analyzer/` - Audio analysis scripts +- `/Users/jakeshore/.clawdbot/workspace/genre-viz/start-genre-universe.sh` - Startup script + +## Key Insights + +Das bridges the **bedroom pop / singer-songwriter world** with the **bass music world** in a way not many artists do. His organic score is high despite being in electronic music because: +- He actually SINGS, not just processed vox samples +- Pop songwriting structure (verse-chorus-bridge) +- Harmonic richness from melodic content + +His lane: **"Organic core + electronic layers"** = singer-songwriter who produces bass music rather than bass producer who adds vocals. + +## Sub-genre Name Ideas Generated + +1. **Tidal Bass** - Oceanic/Polynesian groove + bass foundation +2. **Pacific Melodic** - Geographic nod + melodic bass core +3. **Ethereal Catharsis** - Describes the emotional release function +4. **Sunset Bass** - Golden hour energy, beautiful-sad +5. **Tearwave** - Play on synthwave, emphasizes emotional release + +## Product Vision + +Could become a real product where artists: +1. Connect Spotify OAuth โ†’ auto-pull audio features +2. Upload track โ†’ AI analyzes audio +3. Get positioned in 3D space +4. Receive AI-generated sub-genre names + positioning insights + +## TODO + +- [ ] Restore 56 artists from Discord history +- [ ] Deploy to permanent URL (Cloudflare named tunnel) +- [ ] Add more artists to fill gaps +- [ ] Build audio upload feature for new artist positioning diff --git a/memory/2026-01-26.md b/memory/2026-01-26.md new file mode 100644 index 0000000..bbf5875 --- /dev/null +++ b/memory/2026-01-26.md @@ -0,0 +1,79 @@ +# 2026-01-26 + +## Genre Universe - 3D Artist Visualization + +Built a full Three.js interactive 3D visualization showing where Das sits in genre space relative to other artists. See `2026-01-26-genre-universe.md` for full details. + +**Key files:** `~/.clawdbot/workspace/genre-viz/` +**Live:** http://localhost:8420 + +--- + +## PageIndex Memory System + +Implemented PageIndex-style hierarchical memory structure at `memory/INDEX.json`. Tree-based navigation with: +- Node IDs for each knowledge domain +- File references with line numbers +- Summaries and keywords for reasoning-based retrieval +- No vector search - uses tree traversal + reasoning + +--- + +## Reonomy Scraper v13 - Complete Rebuild + +### What We Built Today + +**1. Production Scraper (`reonomy-scraper-v13.js`)** +- Full anti-detection: random delays (3-8s), shuffled property order, occasional breaks +- Daily limits: tracks properties scraped per day, caps at 50/day +- Session management: saves/loads auth state, auto-relogins if expired +- Appends to existing leads file (doesn't overwrite) + +**2. Quick Demo Script (`reonomy-quick-demo.js`)** +- Single-contact extraction in ~60 seconds +- Opens headed browser so you can show someone +- Leaves browser open for inspection + +### How to Run + +```bash +# Demo (show someone how it works) +node ~/.clawdbot/workspace/reonomy-quick-demo.js + +# Production scrape (20 properties, anti-detection) +node ~/.clawdbot/workspace/reonomy-scraper-v13.js + +# Custom limits +MAX_PROPERTIES=5 node ~/.clawdbot/workspace/reonomy-scraper-v13.js +``` + +### Proven Extraction Workflow + +1. Login โ†’ https://app.reonomy.com/#!/login +2. Search with filters โ†’ `/search/{search-id}` +3. Click property card โ†’ `/property/{id}/building` +4. Click "Owner" tab โ†’ `/property/{id}/ownership` +5. Click "View Contacts (X)" โ†’ navigates to company page +6. Click person link โ†’ `/person/{person-id}` +7. Click "Contact" button โ†’ modal with phones/emails +8. Extract from modal: + - Phones: `button "XXX-XXX-XXXX Company Name"` + - Emails: `button "email@domain.com"` + +### Anti-Detection Features +- Random delays 3-8 seconds between actions +- Shuffled property order (not sequential) +- 20% chance of "coffee break" (8-15s pause) +- 30% chance of random scroll/hover actions +- Daily limit of 50 properties +- Session reuse (doesn't login/logout constantly) + +### Files +- `/Users/jakeshore/.clawdbot/workspace/reonomy-scraper-v13.js` - Production +- `/Users/jakeshore/.clawdbot/workspace/reonomy-quick-demo.js` - Demo +- `/Users/jakeshore/.clawdbot/workspace/reonomy-leads-v13.json` - Output +- `/Users/jakeshore/.clawdbot/workspace/reonomy-daily-stats.json` - Daily tracking +- `/Users/jakeshore/.clawdbot/workspace/reonomy-auth.json` - Saved session + +### Search ID (with phone+email filters) +`bacfd104-fed5-4cc4-aba1-933f899de3f8` - FL Multifamily with phone filter diff --git a/memory/INDEX.json b/memory/INDEX.json new file mode 100644 index 0000000..7190074 --- /dev/null +++ b/memory/INDEX.json @@ -0,0 +1,185 @@ +{ + "doc_name": "Buba Memory System", + "version": "1.0.0", + "created": "2026-01-26", + "description": "PageIndex-style hierarchical memory structure for Clawdbot agent. Enables reasoning-based retrieval over knowledge domains.", + "structure": [ + { + "title": "Projects", + "node_id": "0100", + "summary": "Active and past projects Jake is working on", + "nodes": [ + { + "title": "Remix Sniper (Remi)", + "node_id": "0101", + "file": "2026-01-19-remix-sniper-setup.md", + "summary": "Discord bot scanning music charts for remix opportunities. Auto-runs daily scans, weekly stats.", + "keywords": ["discord", "bot", "music", "spotify", "charts", "remix", "remi"] + }, + { + "title": "Genre Universe Visualization", + "node_id": "0102", + "file": "2026-01-26-genre-universe.md", + "summary": "Three.js 3D visualization showing artist positioning in genre space. Built for Das. 56 artists mapped.", + "keywords": ["threejs", "3d", "visualization", "das", "genre", "artists", "music"] + }, + { + "title": "Burton Method (LSAT EdTech)", + "node_id": "0103", + "file": "burton-method-research-intel.md", + "summary": "LSAT tutoring platform with AI-driven customization. Logical reasoning flowcharts.", + "keywords": ["lsat", "edtech", "tutoring", "education", "burton"] + }, + { + "title": "CRE Sync CRM / Real Connect V2", + "node_id": "0104", + "file": null, + "summary": "Real estate CRM with conditional onboarding flow, Supabase admin visibility, Calendly integration.", + "keywords": ["crm", "real estate", "supabase", "onboarding"] + }, + { + "title": "Reonomy Scraper", + "node_id": "0105", + "file": "2026-01-15.md", + "line_start": 50, + "summary": "Browser automation to extract property owner contacts from Reonomy. Multiple versions (v9-v13).", + "keywords": ["reonomy", "scraper", "real estate", "contacts", "puppeteer", "agent-browser"] + } + ] + }, + { + "title": "People & Contacts", + "node_id": "0200", + "summary": "Key people, contacts, and relationships", + "nodes": [ + { + "title": "Jake Shore (User)", + "node_id": "0201", + "file": "../USER.md", + "summary": "Primary user. Builder/operator. Runs edtech, real estate CRM, music management, automation projects.", + "keywords": ["jake", "user", "owner"] + }, + { + "title": "Das (Artist)", + "node_id": "0202", + "file": "2026-01-26-genre-universe.md", + "summary": "Music artist Jake manages. @das-wav on SoundCloud. Los Angeles. Melodic bass with organic songwriting.", + "keywords": ["das", "artist", "music", "soundcloud", "melodic bass"] + }, + { + "title": "Discord Contacts", + "node_id": "0203", + "file": "CRITICAL-REFERENCE.md", + "summary": "Discord user IDs and server references", + "keywords": ["discord", "contacts", "servers"] + } + ] + }, + { + "title": "Security & Rules", + "node_id": "0300", + "summary": "Security protocols, access rules, incident history", + "nodes": [ + { + "title": "Core Security Rules", + "node_id": "0301", + "file": "../SOUL.md", + "summary": "ABSOLUTE SECURITY RULE #1. Only trust Jake (Discord 938238002528911400, Phone 914-500-9208). Password gating for iMessage.", + "keywords": ["security", "password", "trust", "verification"] + }, + { + "title": "iMessage Security", + "node_id": "0302", + "file": "imessage-security-rules.md", + "summary": "Password JAJAJA2026 required. Mention gating (Buba). Never reveal password.", + "keywords": ["imessage", "password", "bluebubbles"] + }, + { + "title": "Security Incident 2026-01-25", + "node_id": "0303", + "file": "2026-01-25.md", + "summary": "Reed breach incident. Contact memory poisoning. Password leaked. Rules updated.", + "keywords": ["breach", "incident", "reed", "security"] + } + ] + }, + { + "title": "Tools & Skills", + "node_id": "0400", + "summary": "External tools, CLIs, and skills learned", + "nodes": [ + { + "title": "agent-browser", + "node_id": "0401", + "file": "2026-01-15.md", + "line_start": 100, + "summary": "Vercel Labs headless browser CLI. Ref-based navigation, semantic locators, state persistence.", + "keywords": ["browser", "automation", "playwright", "scraping"] + }, + { + "title": "GOG (Google Workspace)", + "node_id": "0402", + "file": "2026-01-14.md", + "summary": "Google Workspace CLI. 3 accounts configured: jake@burtonmethod.com, jake@localbosses.org, jakeshore98@gmail.com", + "keywords": ["google", "gmail", "calendar", "drive", "gog"] + } + ] + }, + { + "title": "Daily Logs", + "node_id": "0500", + "summary": "Chronological daily memory logs", + "nodes": [ + { + "title": "2026-01-26", + "node_id": "0501", + "file": "2026-01-26.md", + "summary": "Reonomy v13 scraper. Genre Universe visualization for Das.", + "keywords": ["reonomy", "genre", "das", "scraper"] + }, + { + "title": "2026-01-25", + "node_id": "0502", + "file": "2026-01-25.md", + "summary": "Security breach incident. Reed contact poisoning. Password rotation.", + "keywords": ["security", "breach", "reed"] + }, + { + "title": "2026-01-19", + "node_id": "0503", + "file": "2026-01-19-remix-sniper-setup.md", + "summary": "Remix Sniper bot setup. PostgreSQL, cron jobs, launchd.", + "keywords": ["remix", "sniper", "postgres", "cron"] + }, + { + "title": "2026-01-15", + "node_id": "0504", + "file": "2026-01-15.md", + "summary": "agent-browser setup. Reonomy URL research. Video clip editing.", + "keywords": ["agent-browser", "reonomy", "video"] + }, + { + "title": "2026-01-14", + "node_id": "0505", + "file": "2026-01-14.md", + "summary": "GOG configuration. Memory system initialized.", + "keywords": ["gog", "memory", "init"] + } + ] + }, + { + "title": "Research Intel", + "node_id": "0600", + "summary": "Ongoing research and competitor tracking", + "nodes": [ + { + "title": "Burton Method Research", + "node_id": "0601", + "file": "burton-method-research-intel.md", + "summary": "Weekly competitor + EdTech trends digest. Current week detail at top, compressed summaries below.", + "keywords": ["burton", "lsat", "competitors", "edtech", "research"] + } + ] + } + ] +} diff --git a/memory/burton-method-research-intel.md b/memory/burton-method-research-intel.md new file mode 100644 index 0000000..16d78be --- /dev/null +++ b/memory/burton-method-research-intel.md @@ -0,0 +1,90 @@ +# Burton Method Research Intel + +> **How this works:** Current week's in-depth intel lives at the top. Each week, I compress the previous week into 1-3 sentences and move it to the archive at the bottom. Reference this file when asked about competitor moves, EdTech trends, or strategic action items. + +--- + +## ๐Ÿ“Š Current Week Intel (Week of Jan 20-26, 2026) + +### ๐Ÿšจ CRITICAL: LSAC Format Change +**Reading Comp removed comparative passages** in January 2026 administration. Confirmed by Blueprint and PowerScore. +- **Impact:** Any RC curriculum teaching comparative passage strategy is now outdated +- **Opportunity:** First to fully adapt = trust signal to students + +--- + +### Competitor Movements + +**7Sage** +- Full site redesign launched (better analytics, cleaner UI) +- **NEW FREE FEATURE:** Application tracker showing interview/accept/reject/waitlist outcomes +- $10,000 giveaway promotion tied to tracker +- Heavy ABA 509 report coverage +- ADHD accommodations content series + 1L survival guides +- **Strategic read:** Pushing hard into admissions territory, not just LSAT. Creates stickiness + data network effects. + +**LSAT Demon** +- **"Ugly Mode"** (Jan 19) โ€” transforms interface to match exact official LSAT layout +- Tuition Roll Call on scholarship estimator โ€” visualizes what students actually paid +- Veteran outreach program with dedicated liaison +- **Strategic read:** Daily podcast creates parasocial relationships. Demon is personality-driven; Burton is methodology-driven. Different lanes. + +**PowerScore** +- **Dave Killoran departed** (HUGE personnel change) +- Jon Denning continuing solo, covering January LSAT chaos extensively +- Crystal Ball webinars still running +- **Strategic read:** Industry veteran leaving creates uncertainty. Watch for quality/content changes. + +**Blueprint** +- First to report RC comparative passages removal +- Non-traditional student content (LSAT at 30/40/50+) +- Score plateau breakthrough guides +- 2025-26 admissions cycle predictions +- **Strategic read:** Solid content machine, "fun" brand positioning. + +**Kaplan** +- $200 off all LSAT prep **extended through Jan 26** (expires TODAY) +- Applies to On Demand, Live Online, In Person, and Standard Tutoring +- Bar prep also discounted ($750 off through Feb 27) +- New 2026 edition book with "99th percentile instructor videos" +- **Strategic read:** Mass-market, price-conscious positioning continues. Heavy discounting signals competitive pressure. + +**Magoosh** +- Updated for post-Logic Games LSAT +- Budget positioning continues +- LSAC remote proctoring option coverage + +**LSAC (Official)** +- February 2026 scheduling opened Jan 20 +- January registration closed; score release Jan 28 +- Mainland China testing unavailable for Jan 2026 +- Reminder to disable grammar-checking programs for Argumentative Writing + +--- + +### EdTech Trends (Week of Jan 25) + +| Story | Score | Key Insight | +|-------|-------|-------------| +| AI Can Deepen Learning | 8/10 | AI mistakes spark deeper learning; productive friction > shortcuts | +| Beyond Memorization: Redefining Rigor | 8/10 | LSAT-relevant: adaptability + critical thinking > memorization | +| Teaching Machines to Spot Human Errors | 7/10 | Eedi Labs predicting student misconceptions; human-in-the-loop AI tutoring | +| Learning As Addictive As TikTok? | 7/10 | Dopamine science for engagement; make progress feel attainable | +| What Students Want From Edtech | 6/10 | UX research: clarity > gimmicks; meaningful gamification only | + +--- + +### ๐Ÿ“Œ Identified Action Items + +1. **URGENT:** Update RC content to remove/deprioritize comparative passage strategy +2. **Content opportunity:** Blog post "What the RC Changes Mean for Your Score" โ€” be fast, be definitive +3. **Positioning clarity:** 7Sage โ†’ admissions features, Demon โ†’ personality, Burton โ†’ systematic methodology that transcends format changes +4. **Product opportunity:** Consider "productive friction" AI features that make students think, not just answer +5. **Watch:** PowerScore post-Killoran quality โ€” potential talent acquisition or market share opportunity + +--- + +## ๐Ÿ“š Previous Weeks Archive + +*(No previous weeks yet โ€” this section will grow as weeks pass)* + diff --git a/reonomy-auth.json b/reonomy-auth.json new file mode 100644 index 0000000..0e6ba1f --- /dev/null +++ b/reonomy-auth.json @@ -0,0 +1,193 @@ +{ + "cookies": [ + { + "name": "_csrf", + "value": "6jys7hEbP7ZvJ9xumYoMa5mH", + "domain": "auth.reonomy.com", + "path": "/usernamepassword/login", + "expires": 1770288255.301182, + "httpOnly": true, + "secure": true, + "sameSite": "Lax" + }, + { + "name": "session", + "value": "092da57b-8505-41b5-b444-9544cee051a3", + "domain": "app.reonomy.com", + "path": "/", + "expires": 1772102668.259811, + "httpOnly": true, + "secure": false, + "sameSite": "Lax" + }, + { + "name": "visid_incap_2934206", + "value": "hGnnWIAJT9C6z599eILcQHdFd2kAAAAAQUIPAAAAAAAQjKa9/zwLhqJ20EA9MJkC", + "domain": ".reonomy.com", + "path": "/", + "expires": 1800955975.508283, + "httpOnly": true, + "secure": false, + "sameSite": "Lax" + }, + { + "name": "nlbi_2934206", + "value": "SBh6U50ej1K0ap6KzbmGDgAAAAChcquNLQbeFePzq1How3JY", + "domain": ".reonomy.com", + "path": "/", + "expires": -1, + "httpOnly": true, + "secure": false, + "sameSite": "Lax" + }, + { + "name": "incap_ses_7222_2934206", + "value": "WLKaQxwO50+O/nd/SrM5ZHhFd2kAAAAAaLG8W1BVx506jU6zpH9QEw==", + "domain": ".reonomy.com", + "path": "/", + "expires": -1, + "httpOnly": false, + "secure": false, + "sameSite": "Lax" + }, + { + "name": "did", + "value": "s%3Av0%3A0a8b7115-0048-4409-8b7b-4bd767bc976a.Zu9u2z0d9taFc00P6UkrCZJRHiA5i23IDlQQs1ZeA6w", + "domain": "auth.reonomy.com", + "path": "/", + "expires": 1800981848.857363, + "httpOnly": true, + "secure": true, + "sameSite": "None" + }, + { + "name": "did_compat", + "value": "s%3Av0%3A0a8b7115-0048-4409-8b7b-4bd767bc976a.Zu9u2z0d9taFc00P6UkrCZJRHiA5i23IDlQQs1ZeA6w", + "domain": "auth.reonomy.com", + "path": "/", + "expires": 1800981848.85744, + "httpOnly": true, + "secure": true, + "sameSite": "Lax" + }, + { + "name": "_cfuvid", + "value": "p83RVhJpe5zNfhLqwr.aQIU_VF3XT8ELmqH9L.hC.AI-1769424248845-0.0.1.1-604800000", + "domain": ".auth.reonomy.com", + "path": "/", + "expires": -1, + "httpOnly": true, + "secure": true, + "sameSite": "None" + }, + { + "name": "auth0", + "value": "s%3AWGDFYzynMlIv7-5Kv6NKz-rXUk_PGr4R.RDL9WQo7wn41jSWskmvu21yF6EJq6JZu8PntXVKYlEY", + "domain": "auth.reonomy.com", + "path": "/", + "expires": 1769683467.194398, + "httpOnly": true, + "secure": true, + "sameSite": "None" + }, + { + "name": "auth0_compat", + "value": "s%3AWGDFYzynMlIv7-5Kv6NKz-rXUk_PGr4R.RDL9WQo7wn41jSWskmvu21yF6EJq6JZu8PntXVKYlEY", + "domain": "auth.reonomy.com", + "path": "/", + "expires": 1769683467.194518, + "httpOnly": true, + "secure": true, + "sameSite": "Lax" + }, + { + "name": "_legacy_auth0.is.authenticated", + "value": "true", + "domain": "app.reonomy.com", + "path": "/", + "expires": 1769510668, + "httpOnly": false, + "secure": true, + "sameSite": "Lax" + }, + { + "name": "auth0.is.authenticated", + "value": "true", + "domain": "app.reonomy.com", + "path": "/", + "expires": 1769510668, + "httpOnly": false, + "secure": true, + "sameSite": "None" + }, + { + "name": "_dd_s", + "value": "logs=1&id=fe9aa087-78af-4a1a-b143-48dc2a44b187&created=1769424268806&expire=1769425168806", + "domain": "app.reonomy.com", + "path": "/", + "expires": 1769425168, + "httpOnly": false, + "secure": false, + "sameSite": "Strict" + }, + { + "name": "ajs_anonymous_id", + "value": "86738591-aeb8-4319-b9ba-8ee8d28bc4df", + "domain": ".reonomy.com", + "path": "/", + "expires": 1800960269, + "httpOnly": false, + "secure": false, + "sameSite": "Lax" + }, + { + "name": "ajs_user_id", + "value": "8243a0ea-0050-45f4-b71e-c59fd20b96a3", + "domain": ".reonomy.com", + "path": "/", + "expires": 1800960269, + "httpOnly": false, + "secure": false, + "sameSite": "Lax" + } + ], + "origins": [ + { + "origin": "https://app.reonomy.com", + "localStorage": [ + { + "name": "_pendo_accountId.e1d67073-c553-48ac-506e-9dad9bfa6af0", + "value": "8243a0ea-0050-45f4-b71e-c59fd20b96a3" + }, + { + "name": "_pendo_visitorId.e1d67073-c553-48ac-506e-9dad9bfa6af0", + "value": "{\"ttl\":1778064268803,\"value\":\"8243a0ea-0050-45f4-b71e-c59fd20b96a3\"}" + }, + { + "name": "@@auth0spajs@@::UTqjIZf5jqE0RoRCJPD216aT9CWZrq2C::https://app.reonomy.com/v2/::openid profile email", + "value": "{\"body\":{\"access_token\":\"eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IlJUWkdOamN3TlVVMFJFTTJORGt6UlRNd1JVSTVSVGs1TlVZeE56UkRNVUUzUlVNd09UTkVOdyJ9.eyJpc3MiOiJodHRwczovL2F1dGgucmVvbm9teS5jb20vIiwic3ViIjoiYXV0aDB8Y2M1YzZhNDctZjU0MS00OWFhLWI0OTItNTkyZmRlN2I4MDAwIiwiYXVkIjpbImh0dHBzOi8vYXBwLnJlb25vbXkuY29tL3YyLyIsImh0dHBzOi8vcmVvbm9teS1wcmQuYXV0aDAuY29tL3VzZXJpbmZvIl0sImlhdCI6MTc2OTQyNDI2OCwiZXhwIjoxNzY5NTEwNjY4LCJzY29wZSI6Im9wZW5pZCBwcm9maWxlIGVtYWlsIiwiYXpwIjoiVVRxaklaZjVqcUUwUm9SQ0pQRDIxNmFUOUNXWnJxMkMifQ.Jj4FAh2o6FJG1zCnh3f9ObEnpbHUQDD5_n2Fh4HwyPAq38o4pHyhYdT9EJGSh37S2mDyJYLRnaf8DXkfncd5j-qnBnGvD4WN1iy9lL7Nf_JSkhfx2CKDewgjurEJXT8K8s_sHuE98wRNXhVUfAofuRrMtmq5LeKOtO0XOAA7xUolluWvUFgtn-M7rooByzpo76Dpg0LJyOOVOBoELxInajREpqQB_C6_CUOam0dNI9SLMmRimRfWSuupGz1ZcQXMNrO0KLdKnspXBjz5l-5ujTGMbxlOQ2r73VuzCFTFrWeeC66UsC-Hau5kflUgRja18NvZOGhMpWmd6t0aSxBJ1A\",\"id_token\":\"eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IlJUWkdOamN3TlVVMFJFTTJORGt6UlRNd1JVSTVSVGs1TlVZeE56UkRNVUUzUlVNd09UTkVOdyJ9.eyJnaXZlbl9uYW1lIjoiSGVucnkiLCJmYW1pbHlfbmFtZSI6IjkwODIxNjY1MzIiLCJuaWNrbmFtZSI6ImhlbnJ5IiwibmFtZSI6ImhlbnJ5QHJlYWxlc3RhdGVlbmhhbmNlZC5jb20iLCJwaWN0dXJlIjoiaHR0cHM6Ly9zLmdyYXZhdGFyLmNvbS9hdmF0YXIvNzMwYTkzZDcwZGMxYzUyMDgyNjhhNGYzODNjNWI1ZmY_cz00ODAmcj1wZyZkPWh0dHBzJTNBJTJGJTJGY2RuLmF1dGgwLmNvbSUyRmF2YXRhcnMlMkZoZS5wbmciLCJ1cGRhdGVkX2F0IjoiMjAyNi0wMS0yNlQxMDo0NDoyMi42NDNaIiwiZW1haWwiOiJoZW5yeUByZWFsZXN0YXRlZW5oYW5jZWQuY29tIiwiaXNzIjoiaHR0cHM6Ly9hdXRoLnJlb25vbXkuY29tLyIsImF1ZCI6IlVUcWpJWmY1anFFMFJvUkNKUEQyMTZhVDlDV1pycTJDIiwic3ViIjoiYXV0aDB8Y2M1YzZhNDctZjU0MS00OWFhLWI0OTItNTkyZmRlN2I4MDAwIiwiaWF0IjoxNzY5NDI0MjY4LCJleHAiOjE3Njk0NjAyNjgsInNpZCI6ImlvRmZCTmVZeWxRWThpTjl0a3FpMGVkQjNSYkhsRldTIiwibm9uY2UiOiJhMFoxZFVKSGQxbFphREpXU2pCUE16aFRaREpWTTJKQ1VIVlBOMU4zVFZWRmFqTXRTSEZzYzNKVmF3PT0ifQ.TMRkNOn8hE3LyI0MmOVs8kfNw3s2uj6fmAB9CmfvYobBluZM-i6RVnMI11qvWtYWaPbzsQ04Mzipr6dWDup9JaJ5_RXaBHG878D7yQi0qAQBOSuK6ZQE_7WTEdP7IDRu-NvfJtbjD6PJmxVrpx713_CAesWNoabJkd4WjOEBIYU-4Xy_p9dkBTdAjhvzcrW7I79Morkw1j_vGc-sr0VMehiXf4PwAxyHR_1Bv7Ythy9ki3YNx-EpfGx8KjPvXndgt_TbC73ZegwUGSsY50hEA3rPTnnfb9QsTGdViy16LaRdov-hXF4ZF8SZRw8TEKAWRJ_my8kicjhLOEOpCDOyRw\",\"scope\":\"openid profile email\",\"expires_in\":86400,\"token_type\":\"Bearer\",\"decodedToken\":{\"encoded\":{\"header\":\"eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IlJUWkdOamN3TlVVMFJFTTJORGt6UlRNd1JVSTVSVGs1TlVZeE56UkRNVUUzUlVNd09UTkVOdyJ9\",\"payload\":\"eyJnaXZlbl9uYW1lIjoiSGVucnkiLCJmYW1pbHlfbmFtZSI6IjkwODIxNjY1MzIiLCJuaWNrbmFtZSI6ImhlbnJ5IiwibmFtZSI6ImhlbnJ5QHJlYWxlc3RhdGVlbmhhbmNlZC5jb20iLCJwaWN0dXJlIjoiaHR0cHM6Ly9zLmdyYXZhdGFyLmNvbS9hdmF0YXIvNzMwYTkzZDcwZGMxYzUyMDgyNjhhNGYzODNjNWI1ZmY_cz00ODAmcj1wZyZkPWh0dHBzJTNBJTJGJTJGY2RuLmF1dGgwLmNvbSUyRmF2YXRhcnMlMkZoZS5wbmciLCJ1cGRhdGVkX2F0IjoiMjAyNi0wMS0yNlQxMDo0NDoyMi42NDNaIiwiZW1haWwiOiJoZW5yeUByZWFsZXN0YXRlZW5oYW5jZWQuY29tIiwiaXNzIjoiaHR0cHM6Ly9hdXRoLnJlb25vbXkuY29tLyIsImF1ZCI6IlVUcWpJWmY1anFFMFJvUkNKUEQyMTZhVDlDV1pycTJDIiwic3ViIjoiYXV0aDB8Y2M1YzZhNDctZjU0MS00OWFhLWI0OTItNTkyZmRlN2I4MDAwIiwiaWF0IjoxNzY5NDI0MjY4LCJleHAiOjE3Njk0NjAyNjgsInNpZCI6ImlvRmZCTmVZeWxRWThpTjl0a3FpMGVkQjNSYkhsRldTIiwibm9uY2UiOiJhMFoxZFVKSGQxbFphREpXU2pCUE16aFRaREpWTTJKQ1VIVlBOMU4zVFZWRmFqTXRTSEZzYzNKVmF3PT0ifQ\",\"signature\":\"TMRkNOn8hE3LyI0MmOVs8kfNw3s2uj6fmAB9CmfvYobBluZM-i6RVnMI11qvWtYWaPbzsQ04Mzipr6dWDup9JaJ5_RXaBHG878D7yQi0qAQBOSuK6ZQE_7WTEdP7IDRu-NvfJtbjD6PJmxVrpx713_CAesWNoabJkd4WjOEBIYU-4Xy_p9dkBTdAjhvzcrW7I79Morkw1j_vGc-sr0VMehiXf4PwAxyHR_1Bv7Ythy9ki3YNx-EpfGx8KjPvXndgt_TbC73ZegwUGSsY50hEA3rPTnnfb9QsTGdViy16LaRdov-hXF4ZF8SZRw8TEKAWRJ_my8kicjhLOEOpCDOyRw\"},\"header\":{\"alg\":\"RS256\",\"typ\":\"JWT\",\"kid\":\"RTZGNjcwNUU0REM2NDkzRTMwRUI5RTk5NUYxNzRDMUE3RUMwOTNENw\"},\"claims\":{\"__raw\":\"eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IlJUWkdOamN3TlVVMFJFTTJORGt6UlRNd1JVSTVSVGs1TlVZeE56UkRNVUUzUlVNd09UTkVOdyJ9.eyJnaXZlbl9uYW1lIjoiSGVucnkiLCJmYW1pbHlfbmFtZSI6IjkwODIxNjY1MzIiLCJuaWNrbmFtZSI6ImhlbnJ5IiwibmFtZSI6ImhlbnJ5QHJlYWxlc3RhdGVlbmhhbmNlZC5jb20iLCJwaWN0dXJlIjoiaHR0cHM6Ly9zLmdyYXZhdGFyLmNvbS9hdmF0YXIvNzMwYTkzZDcwZGMxYzUyMDgyNjhhNGYzODNjNWI1ZmY_cz00ODAmcj1wZyZkPWh0dHBzJTNBJTJGJTJGY2RuLmF1dGgwLmNvbSUyRmF2YXRhcnMlMkZoZS5wbmciLCJ1cGRhdGVkX2F0IjoiMjAyNi0wMS0yNlQxMDo0NDoyMi42NDNaIiwiZW1haWwiOiJoZW5yeUByZWFsZXN0YXRlZW5oYW5jZWQuY29tIiwiaXNzIjoiaHR0cHM6Ly9hdXRoLnJlb25vbXkuY29tLyIsImF1ZCI6IlVUcWpJWmY1anFFMFJvUkNKUEQyMTZhVDlDV1pycTJDIiwic3ViIjoiYXV0aDB8Y2M1YzZhNDctZjU0MS00OWFhLWI0OTItNTkyZmRlN2I4MDAwIiwiaWF0IjoxNzY5NDI0MjY4LCJleHAiOjE3Njk0NjAyNjgsInNpZCI6ImlvRmZCTmVZeWxRWThpTjl0a3FpMGVkQjNSYkhsRldTIiwibm9uY2UiOiJhMFoxZFVKSGQxbFphREpXU2pCUE16aFRaREpWTTJKQ1VIVlBOMU4zVFZWRmFqTXRTSEZzYzNKVmF3PT0ifQ.TMRkNOn8hE3LyI0MmOVs8kfNw3s2uj6fmAB9CmfvYobBluZM-i6RVnMI11qvWtYWaPbzsQ04Mzipr6dWDup9JaJ5_RXaBHG878D7yQi0qAQBOSuK6ZQE_7WTEdP7IDRu-NvfJtbjD6PJmxVrpx713_CAesWNoabJkd4WjOEBIYU-4Xy_p9dkBTdAjhvzcrW7I79Morkw1j_vGc-sr0VMehiXf4PwAxyHR_1Bv7Ythy9ki3YNx-EpfGx8KjPvXndgt_TbC73ZegwUGSsY50hEA3rPTnnfb9QsTGdViy16LaRdov-hXF4ZF8SZRw8TEKAWRJ_my8kicjhLOEOpCDOyRw\",\"given_name\":\"Henry\",\"family_name\":\"9082166532\",\"nickname\":\"henry\",\"name\":\"henry@realestateenhanced.com\",\"picture\":\"https://s.gravatar.com/avatar/730a93d70dc1c5208268a4f383c5b5ff?s=480&r=pg&d=https%3A%2F%2Fcdn.auth0.com%2Favatars%2Fhe.png\",\"updated_at\":\"2026-01-26T10:44:22.643Z\",\"email\":\"henry@realestateenhanced.com\",\"iss\":\"https://auth.reonomy.com/\",\"aud\":\"UTqjIZf5jqE0RoRCJPD216aT9CWZrq2C\",\"sub\":\"auth0|cc5c6a47-f541-49aa-b492-592fde7b8000\",\"iat\":1769424268,\"exp\":1769460268,\"sid\":\"ioFfBNeYylQY8iN9tkqi0edB3RbHlFWS\",\"nonce\":\"a0Z1dUJHd1lZaDJWSjBPMzhTZDJVM2JCUHVPN1N3TVVFajMtSHFsc3JVaw==\"},\"user\":{\"given_name\":\"Henry\",\"family_name\":\"9082166532\",\"nickname\":\"henry\",\"name\":\"henry@realestateenhanced.com\",\"picture\":\"https://s.gravatar.com/avatar/730a93d70dc1c5208268a4f383c5b5ff?s=480&r=pg&d=https%3A%2F%2Fcdn.auth0.com%2Favatars%2Fhe.png\",\"updated_at\":\"2026-01-26T10:44:22.643Z\",\"email\":\"henry@realestateenhanced.com\",\"sub\":\"auth0|cc5c6a47-f541-49aa-b492-592fde7b8000\"}},\"audience\":\"https://app.reonomy.com/v2/\",\"client_id\":\"UTqjIZf5jqE0RoRCJPD216aT9CWZrq2C\"},\"expiresAt\":1769460268}" + }, + { + "name": "_pendo_meta.e1d67073-c553-48ac-506e-9dad9bfa6af0", + "value": "2730005740" + }, + { + "name": "_pendo_guides_blocked.e1d67073-c553-48ac-506e-9dad9bfa6af0", + "value": "{\"ttl\":1769426069089,\"value\":\"1\"}" + }, + { + "name": "ajs_anonymous_id", + "value": "\"86738591-aeb8-4319-b9ba-8ee8d28bc4df\"" + }, + { + "name": "ajs_user_id", + "value": "\"8243a0ea-0050-45f4-b71e-c59fd20b96a3\"" + }, + { + "name": "ajs_user_traits", + "value": "{\"firstName\":\"Henry\",\"lastName\":\"9082166532\",\"email\":\"henry@realestateenhanced.com\",\"isSegmented\":true}" + } + ] + } + ] +} \ No newline at end of file diff --git a/reonomy-demo-video/STORYBOARD.md b/reonomy-demo-video/STORYBOARD.md new file mode 100644 index 0000000..97e99e8 --- /dev/null +++ b/reonomy-demo-video/STORYBOARD.md @@ -0,0 +1,322 @@ +# Reonomy Contact Extractor - Video Storyboard + +**Duration:** 37 seconds (1100 frames @ 30fps) +**Resolution:** 1920 ร— 1080 +**Style:** Product demo with Canvas Viewport technique (camera moves WITHIN screenshots) + +--- + +## Scene 1: Title Card +**Frames:** 0โ€“120 (4 seconds) +**Source:** Generated background (gradient) + +### Visual Content +- Deep purple-to-indigo gradient background (`#0f172a โ†’ #1e1b4b โ†’ #0f172a`) +- Centered title: "๐Ÿข Reonomy Contact Extractor" +- Subtitle below: "CRE Leads โ†’ Your CRM โ€ฆ On Demand" + +### Motion +- **Title animation:** Fades in + slides up from 40px below (spring damping: 25) +- **Subtitle animation:** Same motion, delayed 25 frames (staggered entrance) +- **Background:** Subtle static gradient, no movement + +### Typography +- Title: 72px, weight 800, white +- Subtitle: 28px, weight 400, slate-400 (`#94a3b8`) + +### Transition Out +- Crossfade (20 frames overlap with next scene) + +--- + +## Scene 2: Login +**Frames:** 110โ€“260 (5 seconds) +**Source:** `02-login-filled.png` (Reonomy login page with email pre-filled) + +### Visual Content +- Reonomy login modal showing: + - Logo at top + - "Log In" / "Sign Up" tabs + - "Sign in with Google" button + - "Sign in with Salesforce" button + - Email field showing `henry@realestateenhanced.com` +- Step badge in bottom-left: "1" + "Login to Reonomy" + +### Motion (Canvas Viewport) +| Time | X | Y | Zoom | What's Visible | +|------|---|---|------|----------------| +| Start | 0.5 | 0.2 | 2.0ร— | Reonomy logo and top of login form | +| End | 0.5 | 0.7 | 2.5ร— | Login buttons and email field (zoomed tight) | + +**Camera movement:** Slow vertical pan DOWN the login form, zooming IN as it descends to draw attention to the authentication area. + +### Overlays +- Radial vignette (transparent center, 50% black at edges) +- Step badge animates in with spring (delay: 10 frames after scene start) + +### Audio Cue (if added) +- Soft "whoosh" on scene entrance + +--- + +## Scene 3: Search Interface +**Frames:** 250โ€“400 (5 seconds) +**Source:** `04-search-results.png` (Reonomy home/search page) + +### Visual Content +- Reonomy search interface showing: + - Navigation bar with "Saved Searches", "Data Upload" + - Hero section: "What would you like to search for today?" + - Large search input with placeholder text + - Illustrated cityscape background +- Step badge: "2" + "Search & Filter" + +### Motion (Canvas Viewport) +| Time | X | Y | Zoom | What's Visible | +|------|---|---|------|----------------| +| Start | 0.0 | 0.0 | 1.8ร— | Top-left: nav bar and start of search area | +| End | 0.3 | 0.5 | 1.6ร— | Centered on search bar, slight pull-back to show context | + +**Camera movement:** Diagonal drift from top-left corner toward center, slight zoom OUT to reveal the full search interface. Creates sense of "arriving" at the workspace. + +### Overlays +- Same vignette +- Step badge with spring animation + +--- + +## Scene 4: Property Selection +**Frames:** 390โ€“530 (4.7 seconds) +**Source:** `05-property-detail.png` (Property detail page with satellite map) + +### Visual Content +- Property detail page showing: + - Satellite/aerial view of property (Orlando, FL area) + - Yellow property boundary outline on map + - Address: "...od Bay Dr, Orlando, FL 32821" + - Map controls (zoom, rotate, layers) + - Mini map inset on right +- Step badge: "3" + "Select Property" + +### Motion (Canvas Viewport) +| Time | X | Y | Zoom | What's Visible | +|------|---|---|------|----------------| +| Start | 0.3 | 0.0 | 1.6ร— | Top portion: property header and map top | +| End | 0.5 | 0.4 | 1.4ร— | Centered on satellite view, showing property outline | + +**Camera movement:** Gentle drift down and right, slight zoom OUT. Mimics "exploring" the property details. The yellow boundary should be clearly visible mid-scene. + +### Overlays +- Vignette +- Step badge + +--- + +## Scene 5: Owner Tab +**Frames:** 520โ€“660 (4.7 seconds) +**Source:** `06-owner-tab.png` (Property page with Owner tab visible) + +### Visual Content +- Same property view but scrolled/panned to show: + - Satellite map (upper portion) + - Address visible + - Tab navigation at bottom: "Sales", "Debt", "Tax", "Demographics", "Notes" + - Owner tab should be highlighted/selected +- Step badge: "4" + "Owner Tab" + +### Motion (Canvas Viewport) +| Time | X | Y | Zoom | What's Visible | +|------|---|---|------|----------------| +| Start | 0.5 | 0.2 | 1.5ร— | Map and upper property info | +| End | 0.6 | 0.5 | 1.8ร— | Lower portion with tabs visible, zoomed into Owner area | + +**Camera movement:** Pan DOWN and slightly RIGHT while zooming IN. The motion "discovers" the Owner tab, guiding the viewer's eye to click target. + +### Overlays +- Vignette +- Step badge + +--- + +## Scene 6: Contact Extraction +**Frames:** 650โ€“830 (6 seconds) +**Source:** `09-contact-modal.png` (Contact modal with phone numbers and emails) + +### Visual Content +- Contact modal/panel showing: + - "Phone Numbers" header + - Multiple phone contacts with company names: + - 919-469-9553 (Greystone Property Management) + - 727-341-0186 (Seaside Villas Gulfport) + - 903-566-9506 (Apartment Income Reit, L.P.) + - 407-671-2400 (F.P. Management, Inc.) + - Property type pie chart visible in background + - Company name "INTERNATIONAL LLC +5 more companies" +- Step badge: "5" + "Extract Contacts" + +### Motion (Canvas Viewport) +| Time | X | Y | Zoom | What's Visible | +|------|---|---|------|----------------| +| Start | 0.6 | 0.1 | 1.8ร— | Top of contact list, first phone number | +| End | 0.6 | 0.6 | 2.2ร— | Scrolled down, showing multiple contacts | + +**Camera movement:** Vertical scroll DOWN through the contact list while zooming IN. This is the "money shot" โ€” the viewer sees real contact data appearing. Slow, deliberate movement to let them read the numbers. + +### Overlays +- Vignette +- Step badge +- **Key moment:** Camera should pause slightly on each phone number (achieved via easing, not literal pauses) + +--- + +## Scene 7: Results Summary +**Frames:** 820โ€“1100 (9.3 seconds) +**Source:** Generated (React components, not screenshot) + +### Visual Content + +#### Phase 1: Title (frames 820โ€“870) +- "๐ŸŽ‰ Contacts Extracted" title +- Spring animation entrance + +#### Phase 2: Contact Lists (frames 870โ€“980) +Two columns side by side: + +**Left column โ€” Phone Numbers (green accent)** +``` +๐Ÿ“ž Phone Numbers +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ 919-469-9553 โ”‚ +โ”‚ 727-341-0186 โ”‚ +โ”‚ 903-566-9506 โ”‚ +โ”‚ 407-671-2400 โ”‚ +โ”‚ 407-382-2683 โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +**Right column โ€” Email Addresses (purple accent)** +``` +๐Ÿ“ง Email Addresses +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ berrizoro@gmail.com โ”‚ +โ”‚ aberriz@hotmail.com โ”‚ +โ”‚ jasonhitch1@gmail.com โ”‚ +โ”‚ albert@annarborusa.org โ”‚ +โ”‚ albertb@sterlinghousing.com โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +#### Phase 3: Stats (frames 980โ€“1100) +Four stat counters across bottom: + +| Stat | Value | Color | +|------|-------|-------| +| Contacts | 10 | Purple (`#6366f1`) | +| Phones | 5 | Green (`#10b981`) | +| Emails | 5 | Amber (`#f59e0b`) | +| Time | 60s | Pink (`#ec4899`) | + +### Motion +- **Title:** Fade in + slide up (spring, damping 25) +- **Column headers:** Staggered fade in (phones at frame 870, emails at 875) +- **Contact items:** Cascade animation โ€” each item slides in from the side with 8-frame stagger + - Phones: slide in from LEFT (`translateX(-30px โ†’ 0)`) + - Emails: slide in from RIGHT (`translateX(30px โ†’ 0)`) +- **Stats:** Scale up from 0.5ร— to 1ร— with spring, 10-frame stagger between each + +### Typography +- Title: 52px, weight 800, white +- Column headers: 22px, weight 700, accent color +- Contact items: 18px, weight 600, slate-800 on white cards +- Stat values: 48px, weight 800, accent color +- Stat labels: 16px, weight 400, slate-400 + +### Background +- Same gradient as title card (`#0f172a โ†’ #1e1b4b โ†’ #0f172a`) + +### Transition Out +- Global fade to black over final 30 frames + +--- + +## Global Elements + +### Vignette Overlay +Applied to all screenshot scenes (2โ€“6): +```css +background: radial-gradient( + ellipse at center, + transparent 30%, + rgba(0, 0, 0, 0.5) 100% +); +``` +Creates focus on center content and masks rough edges of zoomed screenshots. + +### Step Badge Component +Persistent UI element in bottom-left during screenshot scenes: +- Purple circle (48px) with step number +- Black pill with step description +- Spring entrance animation (damping: 20) +- Positioned: `bottom: 50px, left: 50px` + +### Easing +All camera movements use: `Easing.inOut(Easing.cubic)` +All UI animations use: `spring({ damping: 18-25 })` + +### Color Palette +| Use | Color | Hex | +|-----|-------|-----| +| Background dark | Slate 900 | `#0f172a` | +| Background accent | Indigo 950 | `#1e1b4b` | +| Primary | Indigo 500 | `#6366f1` | +| Secondary | Violet 500 | `#8b5cf6` | +| Success | Emerald 500 | `#10b981` | +| Warning | Amber 500 | `#f59e0b` | +| Accent | Pink 500 | `#ec4899` | +| Text primary | White | `#ffffff` | +| Text secondary | Slate 400 | `#94a3b8` | +| Text on cards | Slate 800 | `#1e293b` | + +--- + +## Asset Checklist + +### Screenshots Needed +- [x] `02-login-filled.png` โ€” Login page with email filled +- [x] `04-search-results.png` โ€” Search/home interface +- [x] `05-property-detail.png` โ€” Property with satellite map +- [x] `06-owner-tab.png` โ€” Property showing owner tab +- [x] `09-contact-modal.png` โ€” Contact list modal + +### Generated Assets +- [ ] Intro background gradient (or use CSS gradient) +- [ ] Outro background gradient (same as intro) + +### Audio (Optional Future) +- [ ] Background music (upbeat, corporate-friendly) +- [ ] Whoosh sounds on scene transitions +- [ ] "Pop" sounds on stat reveals + +--- + +## Implementation Notes + +### Canvas Viewport Technique +Each screenshot scene uses the `CanvasViewport` component which: +1. Renders the image at 2.5ร— the viewport size +2. Positions a clipping viewport over a portion of the image +3. Animates X, Y, and Zoom values to create camera movement +4. Creates the illusion of moving THROUGH the UI rather than just showing it + +### Key Insight +The zoom values (1.4ร— to 2.5ร—) mean we're always cropped IN โ€” never showing the whole screenshot. This: +- Hides any rough edges or UI inconsistencies +- Forces focus on the relevant area +- Creates cinematic "documentary" feel +- Allows lower-res source images to still look sharp + +### Timing Philosophy +- **Scene 1 (Title):** 4 seconds โ€” enough to read, not too long +- **Scenes 2-5 (UI flow):** ~5 seconds each โ€” enough for camera to move meaningfully +- **Scene 6 (Contacts):** 6 seconds โ€” the payoff, give it room +- **Scene 7 (Results):** 9 seconds โ€” celebration, let animations complete, end strong diff --git a/reonomy-demo-video/package.json b/reonomy-demo-video/package.json new file mode 100644 index 0000000..f5f4cc8 --- /dev/null +++ b/reonomy-demo-video/package.json @@ -0,0 +1,20 @@ +{ + "name": "reonomy-demo-video", + "version": "1.0.0", + "description": "Reonomy Contact Extraction Demo Video", + "scripts": { + "start": "remotion studio", + "build": "remotion render ReonomyDemo out/reonomy-demo.mp4", + "render": "remotion render ReonomyDemo out/reonomy-demo.mp4" + }, + "dependencies": { + "@remotion/cli": "^4.0.0", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "remotion": "^4.0.0" + }, + "devDependencies": { + "@types/react": "^18.2.0", + "typescript": "^5.0.0" + } +} diff --git a/reonomy-demo-video/remotion.config.ts b/reonomy-demo-video/remotion.config.ts new file mode 100644 index 0000000..e8d4dba --- /dev/null +++ b/reonomy-demo-video/remotion.config.ts @@ -0,0 +1,4 @@ +import { Config } from "@remotion/cli/config"; + +Config.setVideoImageFormat("jpeg"); +Config.setOverwriteOutput(true); diff --git a/reonomy-demo-video/src/ReonomyCanvasDemo.tsx b/reonomy-demo-video/src/ReonomyCanvasDemo.tsx new file mode 100644 index 0000000..ed8e8f1 --- /dev/null +++ b/reonomy-demo-video/src/ReonomyCanvasDemo.tsx @@ -0,0 +1,576 @@ +import React from "react"; +import { + AbsoluteFill, + Img, + interpolate, + spring, + useCurrentFrame, + useVideoConfig, + Sequence, + staticFile, + Easing, +} from "remotion"; + +// ============================================ +// CANVAS VIEWPORT - Zoom and pan WITHIN an image +// ============================================ +const CanvasViewport: React.FC<{ + src: string; + startX: number; // Starting viewport position (0-1) + startY: number; + startZoom: number; // Starting zoom level + endX: number; // Ending viewport position + endY: number; + endZoom: number; + progress: number; // Animation progress (0-1) +}> = ({ src, startX, startY, startZoom, endX, endY, endZoom, progress }) => { + const { width, height } = useVideoConfig(); + + // Interpolate current position + const x = interpolate(progress, [0, 1], [startX, endX]); + const y = interpolate(progress, [0, 1], [startY, endY]); + const zoom = interpolate(progress, [0, 1], [startZoom, endZoom]); + + // Canvas size (image is rendered larger than viewport) + const canvasWidth = width * zoom; + const canvasHeight = height * zoom; + + // Calculate offset to pan to the target position + const offsetX = (canvasWidth - width) * x; + const offsetY = (canvasHeight - height) * y; + + return ( +
+ +
+ ); +}; + +// ============================================ +// STEP BADGE +// ============================================ +const StepBadge: React.FC<{ + step: number; + text: string; +}> = ({ step, text }) => { + const frame = useCurrentFrame(); + const { fps } = useVideoConfig(); + + const progress = spring({ frame: frame - 10, fps, config: { damping: 20 } }); + + return ( +
+
+ {step} +
+
+ {text} +
+
+ ); +}; + +// ============================================ +// TITLE CARD +// ============================================ +const TitleCard: React.FC<{ + title: string; + subtitle?: string; +}> = ({ title, subtitle }) => { + const frame = useCurrentFrame(); + const { fps } = useVideoConfig(); + + const progress = spring({ frame: frame - 15, fps, config: { damping: 25 } }); + const subtitleProgress = spring({ frame: frame - 40, fps, config: { damping: 25 } }); + + return ( + +
+ {title} +
+ {subtitle && ( +
+ {subtitle} +
+ )} +
+ ); +}; + +// ============================================ +// SCENE WRAPPER with crossfade +// ============================================ +const Scene: React.FC<{ + children: React.ReactNode; + fadeIn?: number; + fadeOut?: number; +}> = ({ children, fadeIn = 20, fadeOut = 20 }) => { + const frame = useCurrentFrame(); + const { durationInFrames } = useVideoConfig(); + + const opacity = Math.min( + interpolate(frame, [0, fadeIn], [0, 1], { extrapolateRight: "clamp" }), + interpolate(frame, [durationInFrames - fadeOut, durationInFrames], [1, 0], { extrapolateLeft: "clamp" }) + ); + + return {children}; +}; + +// ============================================ +// INDIVIDUAL SCENES with canvas viewport movement +// ============================================ + +const SceneLogin: React.FC = () => { + const frame = useCurrentFrame(); + const { durationInFrames } = useVideoConfig(); + + // Eased progress through the scene + const progress = interpolate(frame, [0, durationInFrames], [0, 1], { + easing: Easing.inOut(Easing.cubic), + }); + + return ( + + + + {/* Vignette */} +
+ + + + ); +}; + +const SceneSearch: React.FC = () => { + const frame = useCurrentFrame(); + const { durationInFrames } = useVideoConfig(); + + const progress = interpolate(frame, [0, durationInFrames], [0, 1], { + easing: Easing.inOut(Easing.cubic), + }); + + return ( + + + +
+ + + + ); +}; + +const SceneProperty: React.FC = () => { + const frame = useCurrentFrame(); + const { durationInFrames } = useVideoConfig(); + + const progress = interpolate(frame, [0, durationInFrames], [0, 1], { + easing: Easing.inOut(Easing.cubic), + }); + + return ( + + + +
+ + + + ); +}; + +const SceneOwner: React.FC = () => { + const frame = useCurrentFrame(); + const { durationInFrames } = useVideoConfig(); + + const progress = interpolate(frame, [0, durationInFrames], [0, 1], { + easing: Easing.inOut(Easing.cubic), + }); + + return ( + + + +
+ + + + ); +}; + +const SceneContacts: React.FC = () => { + const frame = useCurrentFrame(); + const { durationInFrames } = useVideoConfig(); + + const progress = interpolate(frame, [0, durationInFrames], [0, 1], { + easing: Easing.inOut(Easing.cubic), + }); + + return ( + + + +
+ + + + ); +}; + +const SceneResults: React.FC = () => { + const frame = useCurrentFrame(); + const { fps } = useVideoConfig(); + + const phones = [ + "919-469-9553", + "727-341-0186", + "903-566-9506", + "407-671-2400", + "407-382-2683", + ]; + + const emails = [ + "berrizoro@gmail.com", + "aberriz@hotmail.com", + "jasonhitch1@gmail.com", + "albert@annarborusa.org", + "albertb@sterlinghousing.com", + ]; + + return ( + + + {/* Title */} +
+
+ ๐ŸŽ‰ Contacts Extracted +
+
+ + {/* Two columns */} +
+ {/* Phones */} +
+
+ ๐Ÿ“ž Phone Numbers +
+ {phones.map((phone, i) => { + const p = spring({ frame: frame - 40 - i * 8, fps, config: { damping: 18 } }); + return ( +
+ {phone} +
+ ); + })} +
+ + {/* Emails */} +
+
+ ๐Ÿ“ง Email Addresses +
+ {emails.map((email, i) => { + const p = spring({ frame: frame - 45 - i * 8, fps, config: { damping: 18 } }); + return ( +
+ {email} +
+ ); + })} +
+
+ + {/* Stats */} +
+ {[ + { value: "10", label: "Contacts", color: "#6366f1" }, + { value: "5", label: "Phones", color: "#10b981" }, + { value: "5", label: "Emails", color: "#f59e0b" }, + { value: "60s", label: "Time", color: "#ec4899" }, + ].map((stat, i) => { + const p = spring({ frame: frame - 100 - i * 10, fps, config: { damping: 20 } }); + return ( +
+
{stat.value}
+
{stat.label}
+
+ ); + })} +
+
+
+ ); +}; + +// ============================================ +// MAIN COMPOSITION +// ============================================ +export const ReonomyCanvasDemo: React.FC = () => { + return ( + + {/* Intro */} + + + + + {/* Login - camera moves down to login button */} + + + + + {/* Search - camera pans across search results */} + + + + + {/* Property - camera explores property details */} + + + + + {/* Owner - camera moves to owner section */} + + + + + {/* Contacts - camera pans down contact list */} + + + + + {/* Results */} + + + + + ); +}; diff --git a/reonomy-demo-video/src/ReonomyDemo.tsx b/reonomy-demo-video/src/ReonomyDemo.tsx new file mode 100644 index 0000000..dfeb2a2 --- /dev/null +++ b/reonomy-demo-video/src/ReonomyDemo.tsx @@ -0,0 +1,635 @@ +import React from "react"; +import { + AbsoluteFill, + Img, + interpolate, + spring, + useCurrentFrame, + useVideoConfig, + Series, + staticFile, +} from "remotion"; + +// ============================================ +// SPRING CONFIGS (from best practices) +// ============================================ +const SPRING_SMOOTH = { damping: 200 }; +const SPRING_SNAPPY = { damping: 20, stiffness: 200 }; +const SPRING_BOUNCY = { damping: 12 }; + +// ============================================ +// CAMERA - Simple wrapper, ONE motion at a time +// ============================================ +const Camera: React.FC<{ + children: React.ReactNode; + zoom?: number; + x?: number; + y?: number; +}> = ({ children, zoom = 1, x = 0, y = 0 }) => ( +
+ {children} +
+); + +// ============================================ +// KINETIC TEXT - Word by word reveal +// ============================================ +const KineticText: React.FC<{ + text: string; + delay?: number; + stagger?: number; + style?: React.CSSProperties; +}> = ({ text, delay = 0, stagger = 4, style = {} }) => { + const frame = useCurrentFrame(); + const { fps } = useVideoConfig(); + const words = text.split(" "); + + return ( +
+ {words.map((word, i) => { + const wordDelay = delay + i * stagger; + const progress = spring({ + frame: frame - wordDelay, + fps, + config: SPRING_SNAPPY, + }); + + return ( + + {word} + + ); + })} +
+ ); +}; + +// ============================================ +// BROWSER MOCKUP +// ============================================ +const BrowserMockup: React.FC<{ + src: string; + width?: number; +}> = ({ src, width = 1000 }) => { + const height = width * 0.5625; + return ( +
+
+
+
+
+
+ app.reonomy.com +
+
+ +
+ ); +}; + +// ============================================ +// STEP LABEL +// ============================================ +const StepLabel: React.FC<{ + step: number; + text: string; + color?: string; +}> = ({ step, text, color = "#6366f1" }) => { + const frame = useCurrentFrame(); + const { fps } = useVideoConfig(); + + const progress = spring({ frame: frame - 10, fps, config: SPRING_SNAPPY }); + + return ( +
+
+ + Step {step}: {text} + +
+
+ ); +}; + +// ============================================ +// CONTACT CARD +// ============================================ +const ContactCard: React.FC<{ + type: "phone" | "email"; + value: string; + source?: string; + delay: number; +}> = ({ type, value, source, delay }) => { + const frame = useCurrentFrame(); + const { fps } = useVideoConfig(); + + const progress = spring({ frame: frame - delay, fps, config: SPRING_SNAPPY }); + + return ( +
+
+ {type === "phone" ? "๐Ÿ“ž" : "๐Ÿ“ง"} +
+
+
{value}
+ {source &&
{source}
} +
+
+ ); +}; + +// ============================================ +// ANIMATED STAT +// ============================================ +const AnimatedStat: React.FC<{ + value: number | string; + label: string; + color: string; + delay: number; +}> = ({ value, label, color, delay }) => { + const frame = useCurrentFrame(); + const { fps } = useVideoConfig(); + + const progress = spring({ frame: frame - delay, fps, config: SPRING_SMOOTH }); + const numericValue = + typeof value === "number" ? Math.round(interpolate(progress, [0, 1], [0, value])) : value; + + return ( +
+
{numericValue}
+
{label}
+
+ ); +}; + +// ============================================ +// SCENES +// ============================================ + +const SceneIntro: React.FC = () => { + const frame = useCurrentFrame(); + + // Slow, elegant zoom settle: 1.12 -> 1.0 over longer duration + const zoom = interpolate(frame, [0, 120], [1.12, 1.0], { + extrapolateRight: "clamp", + }); + + return ( + + +
+ +
+ +
+
+
+
+ ); +}; + +const SceneLogin: React.FC = () => { + const frame = useCurrentFrame(); + + // Slow push in toward login area: 0.92 -> 1.08 over 140 frames + const zoom = interpolate(frame, [0, 140], [0.92, 1.08], { + extrapolateRight: "clamp", + }); + + return ( + + + + + + + ); +}; + +const SceneSearch: React.FC = () => { + const frame = useCurrentFrame(); + const { fps } = useVideoConfig(); + + // Slower settle: start wide, ease in over 100 frames + const zoom = interpolate(frame, [0, 100], [0.88, 1.0], { + extrapolateRight: "clamp", + }); + + const filterProgress1 = spring({ frame: frame - 50, fps, config: SPRING_SNAPPY }); + const filterProgress2 = spring({ frame: frame - 70, fps, config: SPRING_SNAPPY }); + + return ( + + +
+ + + {/* Filter badges - staggered */} +
+
+ โœ“ Has Phone +
+
+ โœ“ Has Email +
+
+
+
+ +
+ ); +}; + +const SceneProperty: React.FC = () => { + const frame = useCurrentFrame(); + + // Slow, cinematic pan down over 120 frames + const y = interpolate(frame, [0, 120], [-25, 15], { + extrapolateRight: "clamp", + }); + + return ( + + + + + + + ); +}; + +const SceneOwner: React.FC = () => { + const frame = useCurrentFrame(); + + // Slow zoom in to owner section over 120 frames + const zoom = interpolate(frame, [0, 120], [1.0, 1.12], { + extrapolateRight: "clamp", + }); + + return ( + + + + + + + ); +}; + +const SceneModal: React.FC = () => { + const frame = useCurrentFrame(); + const { fps } = useVideoConfig(); + + // Slower pop in then settle over longer duration + const zoom = interpolate(frame, [0, 50, 100], [0.85, 1.04, 1.0], { + extrapolateRight: "clamp", + }); + + const labelProgress = spring({ frame: frame - 70, fps, config: SPRING_BOUNCY }); + + return ( + + + + + + {/* Success label */} +
+
+ + ๐ŸŽ‰ Contacts Extracted! + +
+
+
+ ); +}; + +const SceneResults: React.FC = () => { + const frame = useCurrentFrame(); + const { fps } = useVideoConfig(); + + // Slow, elegant zoom out over 150 frames + const zoom = interpolate(frame, [0, 150], [1.06, 1.0], { + extrapolateRight: "clamp", + }); + + const phones = [ + { value: "919-469-9553", source: "Greystone Property Mgmt" }, + { value: "727-341-0186", source: "Seaside Villas" }, + { value: "903-566-9506", source: "Apartment Income Reit" }, + { value: "407-671-2400", source: "E R Management" }, + { value: "407-382-2683", source: "Bellagio Apartments" }, + ]; + const emails = [ + { value: "berrizoro@gmail.com" }, + { value: "aberriz@hotmail.com" }, + { value: "jasonhitch1@gmail.com" }, + { value: "albert@annarborusa.org" }, + { value: "albertb@sterlinghousing.com" }, + ]; + + const headerProgress = spring({ frame: frame - 10, fps, config: SPRING_SMOOTH }); + const phoneHeaderProgress = spring({ frame: frame - 60, fps, config: SPRING_SMOOTH }); + const emailHeaderProgress = spring({ frame: frame - 70, fps, config: SPRING_SMOOTH }); + + return ( + + +
+ {/* Title */} +
+
+ + CRE Leads Delivered Direct to Your CRM + +
+
+ + โ€ฆ On Demand ๐Ÿš€ + +
+
+ + {/* Contact columns */} +
+
+
+ ๐Ÿ“ž Phone Numbers +
+ {phones.map((p, i) => ( + + ))} +
+ +
+
+ ๐Ÿ“ง Email Addresses +
+ {emails.map((e, i) => ( + + ))} +
+
+ + {/* Stats footer */} +
+ + + + +
+
+
+
+ ); +}; + +// ============================================ +// MAIN COMPOSITION +// ============================================ +export const ReonomyDemo: React.FC = () => { + const frame = useCurrentFrame(); + const { durationInFrames } = useVideoConfig(); + + // Global fade in/out (20 frames as recommended) + const fadeIn = interpolate(frame, [0, 20], [0, 1], { extrapolateRight: "clamp" }); + const fadeOut = interpolate(frame, [durationInFrames - 20, durationInFrames], [1, 0], { + extrapolateLeft: "clamp", + }); + + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +}; diff --git a/reonomy-demo-video/src/ReonomyDemoExciting.tsx b/reonomy-demo-video/src/ReonomyDemoExciting.tsx new file mode 100644 index 0000000..152197f --- /dev/null +++ b/reonomy-demo-video/src/ReonomyDemoExciting.tsx @@ -0,0 +1,819 @@ +import React from "react"; +import { + AbsoluteFill, + Img, + interpolate, + spring, + useCurrentFrame, + useVideoConfig, + Series, + staticFile, + Easing, +} from "remotion"; + +// ============================================ +// SPRING CONFIGS +// ============================================ +const SPRING_SMOOTH = { damping: 200 }; +const SPRING_SNAPPY = { damping: 15, stiffness: 200 }; +const SPRING_BOUNCY = { damping: 8, stiffness: 150 }; +const SPRING_PUNCH = { damping: 12, stiffness: 300 }; + +// ============================================ +// CAMERA with more dramatic capabilities +// ============================================ +const Camera: React.FC<{ + children: React.ReactNode; + zoom?: number; + x?: number; + y?: number; + rotate?: number; +}> = ({ children, zoom = 1, x = 0, y = 0, rotate = 0 }) => ( +
+ {children} +
+); + +// ============================================ +// GLOW PULSE - adds energy to key moments +// ============================================ +const GlowPulse: React.FC<{ + color: string; + size?: number; + delay?: number; + intensity?: number; +}> = ({ color, size = 400, delay = 0, intensity = 0.5 }) => { + const frame = useCurrentFrame(); + const { fps } = useVideoConfig(); + + const progress = spring({ frame: frame - delay, fps, config: SPRING_PUNCH }); + const pulse = Math.sin((frame - delay) / 8) * 0.2 + 0.8; + + return ( +
+ ); +}; + +// ============================================ +// IMPACT FLASH - visual punch on transitions +// ============================================ +const ImpactFlash: React.FC<{ delay: number; color?: string }> = ({ delay, color = "#ffffff" }) => { + const frame = useCurrentFrame(); + const localFrame = frame - delay; + + if (localFrame < 0 || localFrame > 15) return null; + + const opacity = interpolate(localFrame, [0, 3, 15], [0, 0.4, 0], { extrapolateRight: "clamp" }); + + return ( +
+ ); +}; + +// ============================================ +// PARTICLE BURST - celebratory effect +// ============================================ +const ParticleBurst: React.FC<{ delay: number; count?: number }> = ({ delay, count = 20 }) => { + const frame = useCurrentFrame(); + const { fps, width, height } = useVideoConfig(); + const localFrame = frame - delay; + + if (localFrame < 0 || localFrame > 60) return null; + + const progress = interpolate(localFrame, [0, 60], [0, 1], { extrapolateRight: "clamp" }); + + return ( + <> + {[...Array(count)].map((_, i) => { + const angle = (i / count) * Math.PI * 2; + const distance = 200 + (i % 3) * 100; + const x = Math.cos(angle) * distance * progress; + const y = Math.sin(angle) * distance * progress - (progress * progress * 200); + const opacity = 1 - progress; + const colors = ["#6366f1", "#10b981", "#f59e0b", "#ec4899", "#8b5cf6"]; + + return ( +
+ ); + })} + + ); +}; + +// ============================================ +// KINETIC TEXT - more punchy +// ============================================ +const KineticText: React.FC<{ + text: string; + delay?: number; + stagger?: number; + style?: React.CSSProperties; + punch?: boolean; +}> = ({ text, delay = 0, stagger = 3, style = {}, punch = false }) => { + const frame = useCurrentFrame(); + const { fps } = useVideoConfig(); + const words = text.split(" "); + + return ( +
+ {words.map((word, i) => { + const wordDelay = delay + i * stagger; + const config = punch ? SPRING_PUNCH : SPRING_SNAPPY; + const progress = spring({ frame: frame - wordDelay, fps, config }); + + const scale = punch + ? interpolate(progress, [0, 0.5, 1], [0.3, 1.15, 1]) + : interpolate(progress, [0, 1], [0.8, 1]); + const y = interpolate(progress, [0, 1], [50, 0]); + + return ( + + {word} + + ); + })} +
+ ); +}; + +// ============================================ +// BROWSER MOCKUP with glow option +// ============================================ +const BrowserMockup: React.FC<{ + src: string; + width?: number; + glow?: boolean; + glowColor?: string; +}> = ({ src, width = 1000, glow = false, glowColor = "#6366f1" }) => { + const frame = useCurrentFrame(); + const height = width * 0.5625; + const glowPulse = glow ? 0.3 + Math.sin(frame / 10) * 0.15 : 0; + + return ( +
+
+
+
+
+
+ app.reonomy.com +
+
+ +
+ ); +}; + +// ============================================ +// STEP LABEL - more dynamic +// ============================================ +const StepLabel: React.FC<{ + step: number; + text: string; + color?: string; +}> = ({ step, text, color = "#6366f1" }) => { + const frame = useCurrentFrame(); + const { fps } = useVideoConfig(); + + const progress = spring({ frame: frame - 8, fps, config: SPRING_PUNCH }); + const scale = interpolate(progress, [0, 0.6, 1], [0.5, 1.1, 1]); + + return ( +
+
+ + Step {step}: {text} + +
+
+ ); +}; + +// ============================================ +// CONTACT CARD - snappier entrance +// ============================================ +const ContactCard: React.FC<{ + type: "phone" | "email"; + value: string; + source?: string; + delay: number; +}> = ({ type, value, source, delay }) => { + const frame = useCurrentFrame(); + const { fps } = useVideoConfig(); + + const progress = spring({ frame: frame - delay, fps, config: SPRING_PUNCH }); + const scale = interpolate(progress, [0, 0.6, 1], [0.7, 1.08, 1]); + const x = interpolate(progress, [0, 1], [type === "phone" ? -80 : 80, 0]); + + return ( +
+
+ {type === "phone" ? "๐Ÿ“ž" : "๐Ÿ“ง"} +
+
+
{value}
+ {source &&
{source}
} +
+
+ ); +}; + +// ============================================ +// ANIMATED STAT - with count up and punch +// ============================================ +const AnimatedStat: React.FC<{ + value: number | string; + label: string; + color: string; + delay: number; +}> = ({ value, label, color, delay }) => { + const frame = useCurrentFrame(); + const { fps } = useVideoConfig(); + + const progress = spring({ frame: frame - delay, fps, config: SPRING_PUNCH }); + const scale = interpolate(progress, [0, 0.5, 1], [0.3, 1.2, 1]); + const numericValue = + typeof value === "number" ? Math.round(interpolate(progress, [0, 1], [0, value])) : value; + + return ( +
+
+ {numericValue} +
+
{label}
+
+ ); +}; + +// ============================================ +// FLOATING LIGHT STREAKS +// ============================================ +const LightStreaks: React.FC = () => { + const frame = useCurrentFrame(); + const { width, height } = useVideoConfig(); + + return ( + <> + {[...Array(5)].map((_, i) => { + const speed = 0.5 + (i % 3) * 0.3; + const y = (i * 200 + frame * speed * 2) % (height + 200) - 100; + const opacity = 0.1 + (i % 3) * 0.05; + + return ( +
+ ); + })} + + ); +}; + +// ============================================ +// SCENES - More Exciting versions +// ============================================ + +const SceneIntro: React.FC = () => { + const frame = useCurrentFrame(); + const { fps } = useVideoConfig(); + + // Dramatic zoom out with slight overshoot + const zoomProgress = spring({ frame, fps, config: { damping: 15, stiffness: 80 } }); + const zoom = interpolate(zoomProgress, [0, 1], [1.4, 1.0]); + + // Subtle rotation settle + const rotate = interpolate(frame, [0, 60], [2, 0], { extrapolateRight: "clamp" }); + + return ( + + + + + +
+ +
+ +
+
+
+ + +
+ ); +}; + +const SceneLogin: React.FC = () => { + const frame = useCurrentFrame(); + + // Push in with slight overshoot + const zoom = interpolate(frame, [0, 80, 120], [0.85, 1.12, 1.08], { + extrapolateRight: "clamp", + easing: Easing.out(Easing.cubic), + }); + + return ( + + + + + + + + + ); +}; + +const SceneSearch: React.FC = () => { + const frame = useCurrentFrame(); + const { fps } = useVideoConfig(); + + const zoom = interpolate(frame, [0, 60, 100], [0.8, 1.02, 1.0], { + extrapolateRight: "clamp", + easing: Easing.out(Easing.cubic), + }); + + const filter1 = spring({ frame: frame - 40, fps, config: SPRING_PUNCH }); + const filter2 = spring({ frame: frame - 55, fps, config: SPRING_PUNCH }); + + return ( + + + +
+ + +
+
+ โœ“ Has Phone +
+
+ โœ“ Has Email +
+
+
+
+ + +
+ ); +}; + +const SceneProperty: React.FC = () => { + const frame = useCurrentFrame(); + + // Dramatic pan with zoom + const y = interpolate(frame, [0, 100], [-30, 20], { extrapolateRight: "clamp" }); + const zoom = interpolate(frame, [0, 50, 100], [0.95, 1.05, 1.02], { extrapolateRight: "clamp" }); + + return ( + + + + + + + + + ); +}; + +const SceneOwner: React.FC = () => { + const frame = useCurrentFrame(); + + // Punch zoom in + const zoom = interpolate(frame, [0, 40, 80], [0.95, 1.18, 1.12], { + extrapolateRight: "clamp", + easing: Easing.out(Easing.cubic), + }); + + return ( + + + + + + + + + + ); +}; + +const SceneModal: React.FC = () => { + const frame = useCurrentFrame(); + const { fps } = useVideoConfig(); + + // Big dramatic zoom punch + const zoom = interpolate(frame, [0, 30, 70], [0.7, 1.15, 1.05], { + extrapolateRight: "clamp", + easing: Easing.out(Easing.back(1.5)), + }); + + const labelProgress = spring({ frame: frame - 50, fps, config: SPRING_BOUNCY }); + + return ( + + + + + + + + + {/* Success label with punch */} +
+
+ + ๐ŸŽ‰ Contacts Extracted! + +
+
+ + + +
+ ); +}; + +const SceneResults: React.FC = () => { + const frame = useCurrentFrame(); + const { fps } = useVideoConfig(); + + // Elegant zoom out with slight movement + const zoom = interpolate(frame, [0, 100], [1.1, 1.0], { extrapolateRight: "clamp" }); + const y = interpolate(frame, [0, 100], [20, 0], { extrapolateRight: "clamp" }); + + const phones = [ + { value: "919-469-9553", source: "Greystone Property Mgmt" }, + { value: "727-341-0186", source: "Seaside Villas" }, + { value: "903-566-9506", source: "Apartment Income Reit" }, + { value: "407-671-2400", source: "E R Management" }, + { value: "407-382-2683", source: "Bellagio Apartments" }, + ]; + const emails = [ + { value: "berrizoro@gmail.com" }, + { value: "aberriz@hotmail.com" }, + { value: "jasonhitch1@gmail.com" }, + { value: "albert@annarborusa.org" }, + { value: "albertb@sterlinghousing.com" }, + ]; + + const headerProgress = spring({ frame: frame - 5, fps, config: SPRING_PUNCH }); + + return ( + + + + + +
+ {/* Title with punch */} +
+
+ + CRE Leads Delivered Direct to Your CRM + +
+
+ + โ€ฆ On Demand ๐Ÿš€ + +
+
+ + {/* Contact columns */} +
+
+
+ ๐Ÿ“ž Phone Numbers +
+ {phones.map((p, i) => ( + + ))} +
+ +
+
+ ๐Ÿ“ง Email Addresses +
+ {emails.map((e, i) => ( + + ))} +
+
+ + {/* Stats footer */} +
+ + + + +
+
+
+ + +
+ ); +}; + +// ============================================ +// MAIN COMPOSITION +// ============================================ +export const ReonomyDemoExciting: React.FC = () => { + const frame = useCurrentFrame(); + const { durationInFrames } = useVideoConfig(); + + const fadeIn = interpolate(frame, [0, 15], [0, 1], { extrapolateRight: "clamp" }); + const fadeOut = interpolate(frame, [durationInFrames - 15, durationInFrames], [1, 0], { + extrapolateLeft: "clamp", + }); + + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +}; diff --git a/reonomy-demo-video/src/Root.tsx b/reonomy-demo-video/src/Root.tsx new file mode 100644 index 0000000..d3e0a7a --- /dev/null +++ b/reonomy-demo-video/src/Root.tsx @@ -0,0 +1,35 @@ +import { Composition } from "remotion"; +import { ReonomyDemo } from "./ReonomyDemo"; +import { ReonomyDemoExciting } from "./ReonomyDemoExciting"; +import { ReonomyCanvasDemo } from "./ReonomyCanvasDemo"; + +export const RemotionRoot: React.FC = () => { + return ( + <> + + + + + ); +}; diff --git a/reonomy-demo-video/src/index.ts b/reonomy-demo-video/src/index.ts new file mode 100644 index 0000000..f31c790 --- /dev/null +++ b/reonomy-demo-video/src/index.ts @@ -0,0 +1,4 @@ +import { registerRoot } from "remotion"; +import { RemotionRoot } from "./Root"; + +registerRoot(RemotionRoot); diff --git a/reonomy-demo-video/tsconfig.json b/reonomy-demo-video/tsconfig.json new file mode 100644 index 0000000..7985315 --- /dev/null +++ b/reonomy-demo-video/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "target": "ES2018", + "module": "commonjs", + "jsx": "react-jsx", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "declaration": true, + "declarationMap": true, + "outDir": "./dist" + }, + "include": ["src/**/*"], + "exclude": ["node_modules"] +} diff --git a/reonomy-demo.sh b/reonomy-demo.sh new file mode 100755 index 0000000..e388f12 --- /dev/null +++ b/reonomy-demo.sh @@ -0,0 +1,124 @@ +#!/bin/bash +# Reonomy Demo - Quick single contact extraction +# Usage: ./reonomy-demo.sh +# Shows: Login โ†’ Search โ†’ Property โ†’ Owner โ†’ Contact modal with phones/emails + +set -e + +echo "๐ŸŽฏ Reonomy Contact Extraction Demo" +echo "==================================" +echo "" + +# Config +EMAIL="${REONOMY_EMAIL:-henry@realestateenhanced.com}" +PASSWORD="${REONOMY_PASSWORD:-9082166532}" +SEARCH_ID="${REONOMY_SEARCH_ID:-bacfd104-fed5-4cc4-aba1-933f899de3f8}" + +# Random delay helper +delay() { + local min=$1 + local max=$2 + local ms=$(( (RANDOM % (max - min + 1)) + min )) + sleep $(echo "scale=2; $ms/1000" | bc) +} + +echo "๐Ÿ“ Step 1: Opening Reonomy..." +agent-browser open "https://app.reonomy.com/#!/login" --headed +delay 2000 4000 + +echo "๐Ÿ“ Step 2: Logging in..." +agent-browser snapshot -i > /dev/null +agent-browser fill @e2 "$EMAIL" +delay 500 1000 +agent-browser fill @e3 "$PASSWORD" +delay 500 1000 +agent-browser click @e5 +echo " โณ Waiting for login..." +sleep 12 + +# Check login +URL=$(agent-browser eval "window.location.href" 2>/dev/null) +if [[ "$URL" == *"auth.reonomy"* ]]; then + echo " โŒ Login failed" + exit 1 +fi +echo " โœ… Logged in!" + +echo "" +echo "๐Ÿ“ Step 3: Loading search with filters..." +agent-browser open "https://app.reonomy.com/#!/search/$SEARCH_ID" +delay 6000 8000 + +echo "๐Ÿ“ Step 4: Clicking first property..." +# Get fresh snapshot and click first property-looking button +agent-browser snapshot -i > /tmp/reonomy-demo-snap.txt +# Find a property (address pattern) +PROP_REF=$(grep -oE 'button "[0-9]+ [^"]+" \[ref=e[0-9]+\]' /tmp/reonomy-demo-snap.txt | head -1 | grep -oE 'e[0-9]+') +if [ -z "$PROP_REF" ]; then + # Try another pattern + PROP_REF="e8" +fi +agent-browser click @$PROP_REF +delay 5000 7000 + +echo "๐Ÿ“ Step 5: Clicking Owner tab..." +agent-browser find role tab click --name "Owner" +delay 4000 6000 + +echo "๐Ÿ“ Step 6: Clicking View Contacts..." +agent-browser snapshot -i > /tmp/reonomy-demo-snap.txt +VC_REF=$(grep -oE 'button "View Contacts[^"]*" \[ref=e[0-9]+\]' /tmp/reonomy-demo-snap.txt | grep -oE 'e[0-9]+') +if [ -z "$VC_REF" ]; then + echo " โš ๏ธ No View Contacts button found" + agent-browser screenshot /tmp/reonomy-demo-nocontacts.png + echo " Screenshot: /tmp/reonomy-demo-nocontacts.png" + exit 1 +fi +agent-browser click @$VC_REF +delay 3000 5000 + +echo "๐Ÿ“ Step 7: Finding contact person..." +agent-browser snapshot > /tmp/reonomy-demo-snap.txt +# Look for person link +PERSON_URL=$(grep -oE '/!/person/[a-f0-9-]+' /tmp/reonomy-demo-snap.txt | head -1) +if [ -z "$PERSON_URL" ]; then + echo " โš ๏ธ No person link found, checking for direct contacts..." + agent-browser snapshot -i + exit 0 +fi + +echo " Found: $PERSON_URL" +agent-browser open "https://app.reonomy.com$PERSON_URL" +delay 5000 7000 + +echo "๐Ÿ“ Step 8: Opening contact modal..." +agent-browser snapshot -i > /tmp/reonomy-demo-snap.txt +CONTACT_REF=$(grep -oE 'button "Contact" \[ref=e[0-9]+\]' /tmp/reonomy-demo-snap.txt | grep -oE 'e[0-9]+') +if [ -z "$CONTACT_REF" ]; then + echo " โš ๏ธ No Contact button found" + exit 1 +fi +agent-browser click @$CONTACT_REF +delay 2000 3000 + +echo "" +echo "๐ŸŽ‰ CONTACT INFO EXTRACTED!" +echo "==========================" +agent-browser snapshot -i | grep -E '(button "[0-9]{3}-[0-9]{3}-[0-9]{4}|button "[a-zA-Z0-9._%+-]+@)' | while read line; do + if [[ "$line" == *"@"* ]]; then + EMAIL=$(echo "$line" | grep -oE '[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}') + echo " ๐Ÿ“ง $EMAIL" + else + PHONE=$(echo "$line" | grep -oE '[0-9]{3}-[0-9]{3}-[0-9]{4}') + echo " ๐Ÿ“ž $PHONE" + fi +done + +echo "" +echo "๐Ÿ“ธ Taking screenshot..." +agent-browser screenshot /tmp/reonomy-demo-result.png +echo " Saved: /tmp/reonomy-demo-result.png" + +echo "" +echo "โœ… Demo complete! Browser left open for inspection." +echo " Run 'agent-browser close' when done." diff --git a/reonomy-leads-v13.json b/reonomy-leads-v13.json new file mode 100644 index 0000000..21e53ba --- /dev/null +++ b/reonomy-leads-v13.json @@ -0,0 +1,6 @@ +{ + "scrapeDate": "2026-01-26T10:45:52.897Z", + "searchId": "bacfd104-fed5-4cc4-aba1-933f899de3f8", + "leadCount": 0, + "leads": [] +} \ No newline at end of file diff --git a/reonomy-quick-demo.js b/reonomy-quick-demo.js new file mode 100755 index 0000000..e89c582 --- /dev/null +++ b/reonomy-quick-demo.js @@ -0,0 +1,125 @@ +#!/usr/bin/env node +/** + * Reonomy Quick Demo - Single contact extraction + * Shows the full flow in ~60 seconds + * + * Usage: node reonomy-quick-demo.js + * Or: buba reonomy demo + */ + +const { execSync } = require('child_process'); + +const EMAIL = process.env.REONOMY_EMAIL || 'henry@realestateenhanced.com'; +const PASSWORD = process.env.REONOMY_PASSWORD || '9082166532'; +const SEARCH_ID = process.env.REONOMY_SEARCH_ID || 'bacfd104-fed5-4cc4-aba1-933f899de3f8'; + +function ab(cmd) { + console.log(` โ†’ ${cmd}`); + try { + return execSync(`agent-browser ${cmd}`, { encoding: 'utf8', timeout: 30000 }).trim(); + } catch (e) { + console.log(` โš ๏ธ ${e.message.substring(0, 50)}`); + return null; + } +} + +function wait(ms) { + return new Promise(r => setTimeout(r, ms)); +} + +async function demo() { + console.log('\n๐ŸŽฏ REONOMY CONTACT EXTRACTION DEMO'); + console.log('===================================\n'); + + // Login + console.log('๐Ÿ“ Step 1: Login'); + ab('open "https://app.reonomy.com/#!/login" --headed'); + await wait(4000); + ab(`fill @e2 "${EMAIL}"`); + await wait(500); + ab(`fill @e3 "${PASSWORD}"`); + await wait(500); + ab('click @e5'); + console.log(' โณ Logging in...'); + await wait(12000); + + // Search + console.log('\n๐Ÿ“ Step 2: Load filtered search'); + ab(`open "https://app.reonomy.com/#!/search/${SEARCH_ID}"`); + await wait(8000); + + // Click property + console.log('\n๐Ÿ“ Step 3: Select property'); + const snap1 = ab('snapshot -i'); + const propMatch = snap1?.match(/button "(\d+[^"]+)" \[ref=(e\d+)\]/); + if (propMatch) { + console.log(` Property: ${propMatch[1].substring(0, 50)}...`); + ab(`click @${propMatch[2]}`); + } else { + ab('click @e8'); + } + await wait(6000); + + // Owner tab + console.log('\n๐Ÿ“ Step 4: Owner tab'); + ab('find role tab click --name "Owner"'); + await wait(5000); + + // View Contacts + console.log('\n๐Ÿ“ Step 5: View Contacts'); + const snap2 = ab('snapshot -i'); + const vcMatch = snap2?.match(/button "View Contacts[^"]*" \[ref=(e\d+)\]/); + if (vcMatch) { + ab(`click @${vcMatch[1]}`); + await wait(5000); + } + + // Find person + console.log('\n๐Ÿ“ Step 6: Navigate to person'); + const snap3 = ab('snapshot'); + const personMatch = snap3?.match(/\/url: \/!\/person\/([a-f0-9-]+)/); + if (personMatch) { + ab(`open "https://app.reonomy.com/!/person/${personMatch[1]}"`); + await wait(6000); + } + + // Click Contact + console.log('\n๐Ÿ“ Step 7: Open contact modal'); + const snap4 = ab('snapshot -i'); + const contactMatch = snap4?.match(/button "Contact" \[ref=(e\d+)\]/); + if (contactMatch) { + ab(`click @${contactMatch[1]}`); + await wait(3000); + } + + // Extract + console.log('\n๐Ÿ“ Step 8: EXTRACT CONTACTS'); + const snap5 = ab('snapshot -i'); + + console.log('\n๐ŸŽ‰ CONTACTS FOUND:'); + console.log('=================='); + + // Phones + const phones = snap5?.matchAll(/button "(\d{3}-\d{3}-\d{4})\s+([^"]+)"/g) || []; + for (const p of phones) { + console.log(` ๐Ÿ“ž ${p[1]} (${p[2].substring(0, 30)})`); + } + + // Emails + const emails = snap5?.matchAll(/button "([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})"/g) || []; + for (const e of emails) { + console.log(` ๐Ÿ“ง ${e[1]}`); + } + + // Screenshot + ab('screenshot /tmp/reonomy-demo-final.png'); + console.log('\n๐Ÿ“ธ Screenshot: /tmp/reonomy-demo-final.png'); + + console.log('\nโœ… Demo complete! Browser left open.'); + console.log(' Run: agent-browser close'); +} + +demo().catch(e => { + console.error('Demo failed:', e.message); + process.exit(1); +}); diff --git a/reonomy-scraper-v13.js b/reonomy-scraper-v13.js new file mode 100755 index 0000000..54ccfb0 --- /dev/null +++ b/reonomy-scraper-v13.js @@ -0,0 +1,442 @@ +#!/usr/bin/env node +/** + * Reonomy Scraper v13 - Agent-Browser Edition (Anti-Detection) + * + * ANTI-DETECTION FEATURES: + * - Random delays (human-like timing) + * - Random property order + * - Occasional "distraction" actions + * - Session limits (max per run) + * - Daily tracking to avoid over-scraping + */ + +const { execSync } = require('child_process'); +const fs = require('fs'); +const path = require('path'); + +// Config +const CONFIG = { + authStatePath: path.join(process.env.HOME, '.clawdbot/workspace/reonomy-auth.json'), + outputPath: path.join(process.env.HOME, '.clawdbot/workspace/reonomy-leads-v13.json'), + logPath: path.join(process.env.HOME, '.clawdbot/workspace/reonomy-scraper-v13.log'), + dailyLogPath: path.join(process.env.HOME, '.clawdbot/workspace/reonomy-daily-stats.json'), + searchId: process.env.REONOMY_SEARCH_ID || 'bacfd104-fed5-4cc4-aba1-933f899de3f8', + maxProperties: parseInt(process.env.MAX_PROPERTIES) || 20, + maxDailyProperties: 50, // Don't exceed this per day + headless: process.env.HEADLESS !== 'false', + email: process.env.REONOMY_EMAIL || 'henry@realestateenhanced.com', + password: process.env.REONOMY_PASSWORD || '9082166532', +}; + +// Anti-detection: Random delay between min and max ms +function randomDelay(minMs, maxMs) { + const delay = Math.floor(Math.random() * (maxMs - minMs + 1)) + minMs; + return new Promise(resolve => setTimeout(resolve, delay)); +} + +// Anti-detection: Shuffle array (Fisher-Yates) +function shuffle(array) { + const arr = [...array]; + for (let i = arr.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + [arr[i], arr[j]] = [arr[j], arr[i]]; + } + return arr; +} + +// Logging +function log(msg) { + const timestamp = new Date().toISOString(); + const line = `[${timestamp}] ${msg}`; + console.log(line); + fs.appendFileSync(CONFIG.logPath, line + '\n'); +} + +// Run agent-browser command +function ab(cmd, options = {}) { + const fullCmd = `agent-browser ${cmd}`; + if (options.verbose !== false) { + log(` ๐Ÿ”ง ${fullCmd}`); + } + try { + const result = execSync(fullCmd, { + encoding: 'utf8', + timeout: options.timeout || 30000, + stdio: ['pipe', 'pipe', 'pipe'] + }); + return { success: true, output: result.trim() }; + } catch (err) { + const stderr = err.stderr?.toString() || err.message; + if (options.verbose !== false) { + log(` โŒ Error: ${stderr.substring(0, 100)}`); + } + return { success: false, error: stderr }; + } +} + +// Anti-detection: Random "human" actions +async function humanize() { + const actions = [ + () => ab('scroll down 200', { verbose: false }), + () => ab('scroll up 100', { verbose: false }), + () => randomDelay(500, 1500), + () => randomDelay(1000, 2000), + ]; + + // 30% chance to do a random action + if (Math.random() < 0.3) { + const action = actions[Math.floor(Math.random() * actions.length)]; + await action(); + } +} + +// Daily stats tracking +function getDailyStats() { + const today = new Date().toISOString().split('T')[0]; + try { + const data = JSON.parse(fs.readFileSync(CONFIG.dailyLogPath, 'utf8')); + if (data.date === today) { + return data; + } + } catch (e) {} + return { date: today, propertiesScraped: 0, leadsFound: 0 }; +} + +function saveDailyStats(stats) { + fs.writeFileSync(CONFIG.dailyLogPath, JSON.stringify(stats, null, 2)); +} + +// Login to Reonomy +async function login() { + log(' Navigating to login page...'); + ab('open "https://app.reonomy.com/#!/login"'); + await randomDelay(3000, 5000); + + const snapshot = ab('snapshot -i'); + if (!snapshot.output?.includes('textbox "Email"')) { + const urlCheck = ab('eval "window.location.href"'); + if (urlCheck.output?.includes('app.reonomy.com') && !urlCheck.output?.includes('login')) { + log(' Already logged in!'); + return true; + } + throw new Error('Login form not found'); + } + + const emailMatch = snapshot.output.match(/textbox "Email" \[ref=(e\d+)\]/); + const passMatch = snapshot.output.match(/textbox "Password" \[ref=(e\d+)\]/); + const loginMatch = snapshot.output.match(/button "Log In" \[ref=(e\d+)\]/); + + if (!emailMatch || !passMatch || !loginMatch) { + throw new Error('Could not find login form elements'); + } + + log(' Filling credentials...'); + ab(`fill @${emailMatch[1]} "${CONFIG.email}"`); + await randomDelay(800, 1500); + ab(`fill @${passMatch[1]} "${CONFIG.password}"`); + await randomDelay(800, 1500); + + log(' Clicking login...'); + ab(`click @${loginMatch[1]}`); + await randomDelay(12000, 16000); // Human-like wait for login + + const postLoginUrl = ab('eval "window.location.href"'); + if (postLoginUrl.output?.includes('auth.reonomy.com') || postLoginUrl.output?.includes('login')) { + throw new Error('Login failed - still on login page'); + } + + log(' Saving auth state...'); + ab(`state save "${CONFIG.authStatePath}"`); + + log(' โœ… Login successful!'); + return true; +} + +// Extract contacts from modal snapshot +function extractContacts(snapshot) { + const phones = []; + const emails = []; + + const phoneMatches = snapshot.matchAll(/button "(\d{3}-\d{3}-\d{4})\s+([^"]+)"/g); + for (const match of phoneMatches) { + phones.push({ number: match[1], source: match[2].trim() }); + } + + const emailMatches = snapshot.matchAll(/button "([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})"/g); + for (const match of emailMatches) { + emails.push(match[1]); + } + + return { phones, emails }; +} + +// Main scraping function +async function scrape() { + log('๐Ÿš€ Starting Reonomy Scraper v13 (ANTI-DETECTION MODE)'); + + // Check daily limits + const dailyStats = getDailyStats(); + if (dailyStats.propertiesScraped >= CONFIG.maxDailyProperties) { + log(`โš ๏ธ Daily limit reached (${dailyStats.propertiesScraped}/${CONFIG.maxDailyProperties}). Try again tomorrow.`); + return []; + } + + const remainingToday = CONFIG.maxDailyProperties - dailyStats.propertiesScraped; + const maxThisRun = Math.min(CONFIG.maxProperties, remainingToday); + log(`๐Ÿ“Š Daily stats: ${dailyStats.propertiesScraped} scraped today, ${remainingToday} remaining`); + log(`๐Ÿ“Š This run: max ${maxThisRun} properties`); + + const leads = []; + + try { + // Step 1: Auth + log('\n๐Ÿ“ Step 1: Authenticating...'); + + let needsLogin = true; + if (fs.existsSync(CONFIG.authStatePath)) { + log(' Found existing auth state, testing...'); + ab(`state load "${CONFIG.authStatePath}"`); + ab('open "https://app.reonomy.com/#!/home"'); + await randomDelay(4000, 6000); + + const testUrl = ab('eval "window.location.href"'); + if (testUrl.output?.includes('app.reonomy.com') && + !testUrl.output?.includes('auth.reonomy.com') && + !testUrl.output?.includes('login')) { + log(' โœ… Session still valid!'); + needsLogin = false; + } else { + log(' โš ๏ธ Session expired...'); + } + } + + if (needsLogin) { + await login(); + } + + // Step 2: Navigate to search + log('\n๐Ÿ“ Step 2: Navigating to search results...'); + const searchUrl = `https://app.reonomy.com/#!/search/${CONFIG.searchId}`; + ab(`open "${searchUrl}"`); + await randomDelay(6000, 10000); + + let urlCheck = ab('eval "window.location.href"'); + if (urlCheck.output?.includes('auth.reonomy.com') || urlCheck.output?.includes('login')) { + log(' Session invalid, logging in...'); + await login(); + ab(`open "${searchUrl}"`); + await randomDelay(6000, 10000); + } + + // Step 3: Get property list + log('\n๐Ÿ“ Step 3: Getting property list...'); + await humanize(); + + const iSnapshot = ab('snapshot -i'); + const properties = []; + + // Find property buttons (addresses) + const buttonMatches = iSnapshot.output?.matchAll(/button "([^"]+)" \[ref=(e\d+)\]/g) || []; + for (const match of buttonMatches) { + if (match[1].includes('Saved Searches') || + match[1].includes('Help Center') || + match[1].includes('More filters') || + match[1].length < 10) { + continue; + } + if (/\d+.*(?:st|ave|blvd|dr|ln|rd|way|ct|highway)/i.test(match[1])) { + properties.push({ + name: match[1].substring(0, 60), + ref: match[2] + }); + } + } + + log(` Found ${properties.length} properties`); + + if (properties.length === 0) { + ab('screenshot /tmp/reonomy-v13-no-properties.png'); + throw new Error('No properties found'); + } + + // Anti-detection: Shuffle and limit + const shuffledProps = shuffle(properties).slice(0, maxThisRun); + log(` Processing ${shuffledProps.length} properties (randomized order)`); + + // Step 4: Process properties + log('\n๐Ÿ“ Step 4: Processing properties...'); + + for (let i = 0; i < shuffledProps.length; i++) { + const prop = shuffledProps[i]; + log(`\n --- Property ${i + 1}/${shuffledProps.length}: ${prop.name.substring(0, 40)}... ---`); + + await humanize(); + + try { + // Click property + ab(`click @${prop.ref}`); + await randomDelay(5000, 8000); + + const propUrl = ab('eval "window.location.href"'); + const propIdMatch = propUrl.output?.match(/property\/([a-f0-9-]+)/); + const propertyId = propIdMatch ? propIdMatch[1] : 'unknown'; + + let propertyAddress = prop.name; + const titleSnap = ab('snapshot'); + const headingMatch = titleSnap.output?.match(/heading "([^"]+)"/); + if (headingMatch) propertyAddress = headingMatch[1]; + + // Click Owner tab + log(' Clicking Owner tab...'); + await humanize(); + ab('find role tab click --name "Owner"'); + await randomDelay(4000, 6000); + + // Find View Contacts + const ownerSnap = ab('snapshot -i'); + const vcMatch = ownerSnap.output?.match(/button "View Contacts \((\d+)\)" \[ref=(e\d+)\]/); + + if (!vcMatch) { + log(' โš ๏ธ No View Contacts button'); + ab('back'); + await randomDelay(3000, 5000); + dailyStats.propertiesScraped++; + continue; + } + + log(` Found ${vcMatch[1]} contacts`); + ab(`click @${vcMatch[2]}`); + await randomDelay(4000, 6000); + + // Find person link + const companySnap = ab('snapshot'); + const personMatch = companySnap.output?.match(/\/url: \/!\/person\/([a-f0-9-]+)/); + + if (!personMatch) { + log(' โš ๏ธ No person link found'); + ab('back'); + await randomDelay(2000, 4000); + ab('back'); + await randomDelay(3000, 5000); + dailyStats.propertiesScraped++; + continue; + } + + const personId = personMatch[1]; + + // Get person name + const personNameMatch = companySnap.output?.match(/link "([^"]+)"[^\n]*\/url: \/!\/person/); + const personName = personNameMatch ? personNameMatch[1] : 'Unknown'; + + log(` Person: ${personName}`); + ab(`open "https://app.reonomy.com/!/person/${personId}"`); + await randomDelay(5000, 8000); + + // Click Contact button + await humanize(); + const personSnap = ab('snapshot -i'); + const contactMatch = personSnap.output?.match(/button "Contact" \[ref=(e\d+)\]/); + + if (!contactMatch) { + log(' โš ๏ธ No Contact button'); + ab('back'); + await randomDelay(3000, 5000); + dailyStats.propertiesScraped++; + continue; + } + + ab(`click @${contactMatch[1]}`); + await randomDelay(2000, 4000); + + // Extract contacts + const modalSnap = ab('snapshot -i'); + const contacts = extractContacts(modalSnap.output || ''); + + log(` ๐Ÿ“ž ${contacts.phones.length} phones, ๐Ÿ“ง ${contacts.emails.length} emails`); + + if (contacts.phones.length > 0 || contacts.emails.length > 0) { + leads.push({ + scrapeDate: new Date().toISOString(), + propertyId, + propertyAddress, + personName, + personId, + phones: contacts.phones, + emails: contacts.emails + }); + dailyStats.leadsFound++; + log(' โœ… Lead captured!'); + } + + dailyStats.propertiesScraped++; + + // Close modal and return to search + ab('press Escape'); + await randomDelay(1000, 2000); + ab(`open "https://app.reonomy.com/#!/search/${CONFIG.searchId}"`); + await randomDelay(5000, 8000); + + // Occasional longer break (anti-detection) + if (Math.random() < 0.2) { + log(' โ˜• Taking a short break...'); + await randomDelay(8000, 15000); + } + + } catch (propError) { + log(` โŒ Error: ${propError.message}`); + ab(`open "https://app.reonomy.com/#!/search/${CONFIG.searchId}"`); + await randomDelay(5000, 8000); + dailyStats.propertiesScraped++; + } + + // Save progress + saveDailyStats(dailyStats); + } + + // Step 5: Save results + log('\n๐Ÿ“ Step 5: Saving results...'); + + // Append to existing leads if file exists + let allLeads = []; + try { + const existing = JSON.parse(fs.readFileSync(CONFIG.outputPath, 'utf8')); + allLeads = existing.leads || []; + } catch (e) {} + + allLeads = [...allLeads, ...leads]; + + const output = { + lastUpdated: new Date().toISOString(), + searchId: CONFIG.searchId, + totalLeads: allLeads.length, + leads: allLeads + }; + + fs.writeFileSync(CONFIG.outputPath, JSON.stringify(output, null, 2)); + log(`โœ… Saved ${leads.length} new leads (${allLeads.length} total)`); + + saveDailyStats(dailyStats); + log(`๐Ÿ“Š Daily total: ${dailyStats.propertiesScraped} properties, ${dailyStats.leadsFound} leads`); + + } catch (error) { + log(`\nโŒ Fatal error: ${error.message}`); + ab('screenshot /tmp/reonomy-v13-error.png'); + throw error; + } finally { + log('\n๐Ÿงน Closing browser...'); + ab('close'); + } + + return leads; +} + +// Run +scrape() + .then(leads => { + log(`\n๐ŸŽ‰ Done! Scraped ${leads.length} leads this run.`); + process.exit(0); + }) + .catch(err => { + log(`\n๐Ÿ’ฅ Scraper failed: ${err.message}`); + process.exit(1); + }); diff --git a/skills/agent-browser/.clawdhub/origin.json b/skills/agent-browser/.clawdhub/origin.json new file mode 100644 index 0000000..6615ff2 --- /dev/null +++ b/skills/agent-browser/.clawdhub/origin.json @@ -0,0 +1,7 @@ +{ + "version": 1, + "registry": "https://clawdhub.com", + "slug": "agent-browser", + "installedVersion": "0.2.0", + "installedAt": 1769423250734 +} diff --git a/skills/agent-browser/CONTRIBUTING.md b/skills/agent-browser/CONTRIBUTING.md new file mode 100644 index 0000000..d44561a --- /dev/null +++ b/skills/agent-browser/CONTRIBUTING.md @@ -0,0 +1,63 @@ +# Contributing to Agent Browser Skill + +This skill wraps the agent-browser CLI. Determine where the problem lies before reporting issues. + +## Issue Reporting Guide + +### Open an issue in this repository if + +- The skill documentation is unclear or missing +- Examples in SKILL.md do not work +- You need help using the CLI with this skill wrapper +- The skill is missing a command or feature + +### Open an issue at the agent-browser repository if + +- The CLI crashes or throws errors +- Commands do not behave as documented +- You found a bug in the browser automation +- You need a new feature in the CLI + +## Before Opening an Issue + +1. Install the latest version + ```bash + npm install -g agent-browser@latest + ``` + +2. Test the command in your terminal to isolate the issue + +## Issue Report Template + +Use this template to provide necessary information. + +```markdown +### Description +[Provide a clear and concise description of the bug] + +### Reproduction Steps +1. [First Step] +2. [Second Step] +3. [Observe error] + +### Expected Behavior +[Describe what you expected to happen] + +### Environment Details +- **Skill Version:** [e.g. 1.0.2] +- **agent-browser Version:** [output of agent-browser --version] +- **Node.js Version:** [output of node -v] +- **Operating System:** [e.g. macOS Sonoma, Windows 11, Ubuntu 22.04] + +### Additional Context +- [Full error output or stack trace] +- [Screenshots] +- [Website URLs where the failure occurred] +``` + +## Adding New Commands to the Skill + +Update SKILL.md when the upstream CLI adds new commands. +- Keep the Installation section +- Add new commands in the correct category +- Include usage examples diff --git a/skills/agent-browser/SKILL.md b/skills/agent-browser/SKILL.md new file mode 100644 index 0000000..85d1ac3 --- /dev/null +++ b/skills/agent-browser/SKILL.md @@ -0,0 +1,328 @@ +--- +name: Agent Browser +description: A fast Rust-based headless browser automation CLI with Node.js fallback that enables AI agents to navigate, click, type, and snapshot pages via structured commands. +read_when: + - Automating web interactions + - Extracting structured data from pages + - Filling forms programmatically + - Testing web UIs +metadata: {"clawdbot":{"emoji":"๐ŸŒ","requires":{"bins":["node","npm"]}}} +allowed-tools: Bash(agent-browser:*) +--- + +# Browser Automation with agent-browser + +## Installation + +### npm recommended + +```bash +npm install -g agent-browser +agent-browser install +agent-browser install --with-deps +``` + +### From Source + +```bash +git clone https://github.com/vercel-labs/agent-browser +cd agent-browser +pnpm install +pnpm build +agent-browser install +``` + +## Quick start + +```bash +agent-browser open # Navigate to page +agent-browser snapshot -i # Get interactive elements with refs +agent-browser click @e1 # Click element by ref +agent-browser fill @e2 "text" # Fill input by ref +agent-browser close # Close browser +``` + +## Core workflow + +1. Navigate: `agent-browser open ` +2. Snapshot: `agent-browser snapshot -i` (returns elements with refs like `@e1`, `@e2`) +3. Interact using refs from the snapshot +4. Re-snapshot after navigation or significant DOM changes + +## Commands + +### Navigation + +```bash +agent-browser open # Navigate to URL +agent-browser back # Go back +agent-browser forward # Go forward +agent-browser reload # Reload page +agent-browser close # Close browser +``` + +### Snapshot (page analysis) + +```bash +agent-browser snapshot # Full accessibility tree +agent-browser snapshot -i # Interactive elements only (recommended) +agent-browser snapshot -c # Compact output +agent-browser snapshot -d 3 # Limit depth to 3 +agent-browser snapshot -s "#main" # Scope to CSS selector +``` + +### Interactions (use @refs from snapshot) + +```bash +agent-browser click @e1 # Click +agent-browser dblclick @e1 # Double-click +agent-browser focus @e1 # Focus element +agent-browser fill @e2 "text" # Clear and type +agent-browser type @e2 "text" # Type without clearing +agent-browser press Enter # Press key +agent-browser press Control+a # Key combination +agent-browser keydown Shift # Hold key down +agent-browser keyup Shift # Release key +agent-browser hover @e1 # Hover +agent-browser check @e1 # Check checkbox +agent-browser uncheck @e1 # Uncheck checkbox +agent-browser select @e1 "value" # Select dropdown +agent-browser scroll down 500 # Scroll page +agent-browser scrollintoview @e1 # Scroll element into view +agent-browser drag @e1 @e2 # Drag and drop +agent-browser upload @e1 file.pdf # Upload files +``` + +### Get information + +```bash +agent-browser get text @e1 # Get element text +agent-browser get html @e1 # Get innerHTML +agent-browser get value @e1 # Get input value +agent-browser get attr @e1 href # Get attribute +agent-browser get title # Get page title +agent-browser get url # Get current URL +agent-browser get count ".item" # Count matching elements +agent-browser get box @e1 # Get bounding box +``` + +### Check state + +```bash +agent-browser is visible @e1 # Check if visible +agent-browser is enabled @e1 # Check if enabled +agent-browser is checked @e1 # Check if checked +``` + +### Screenshots & PDF + +```bash +agent-browser screenshot # Screenshot to stdout +agent-browser screenshot path.png # Save to file +agent-browser screenshot --full # Full page +agent-browser pdf output.pdf # Save as PDF +``` + +### Video recording + +```bash +agent-browser record start ./demo.webm # Start recording (uses current URL + state) +agent-browser click @e1 # Perform actions +agent-browser record stop # Stop and save video +agent-browser record restart ./take2.webm # Stop current + start new recording +``` + +Recording creates a fresh context but preserves cookies/storage from your session. If no URL is provided, it automatically returns to your current page. For smooth demos, explore first, then start recording. + +### Wait + +```bash +agent-browser wait @e1 # Wait for element +agent-browser wait 2000 # Wait milliseconds +agent-browser wait --text "Success" # Wait for text +agent-browser wait --url "/dashboard" # Wait for URL pattern +agent-browser wait --load networkidle # Wait for network idle +agent-browser wait --fn "window.ready" # Wait for JS condition +``` + +### Mouse control + +```bash +agent-browser mouse move 100 200 # Move mouse +agent-browser mouse down left # Press button +agent-browser mouse up left # Release button +agent-browser mouse wheel 100 # Scroll wheel +``` + +### Semantic locators (alternative to refs) + +```bash +agent-browser find role button click --name "Submit" +agent-browser find text "Sign In" click +agent-browser find label "Email" fill "user@test.com" +agent-browser find first ".item" click +agent-browser find nth 2 "a" text +``` + +### Browser settings + +```bash +agent-browser set viewport 1920 1080 # Set viewport size +agent-browser set device "iPhone 14" # Emulate device +agent-browser set geo 37.7749 -122.4194 # Set geolocation +agent-browser set offline on # Toggle offline mode +agent-browser set headers '{"X-Key":"v"}' # Extra HTTP headers +agent-browser set credentials user pass # HTTP basic auth +agent-browser set media dark # Emulate color scheme +``` + +### Cookies & Storage + +```bash +agent-browser cookies # Get all cookies +agent-browser cookies set name value # Set cookie +agent-browser cookies clear # Clear cookies +agent-browser storage local # Get all localStorage +agent-browser storage local key # Get specific key +agent-browser storage local set k v # Set value +agent-browser storage local clear # Clear all +``` + +### Network + +```bash +agent-browser network route # Intercept requests +agent-browser network route --abort # Block requests +agent-browser network route --body '{}' # Mock response +agent-browser network unroute [url] # Remove routes +agent-browser network requests # View tracked requests +agent-browser network requests --filter api # Filter requests +``` + +### Tabs & Windows + +```bash +agent-browser tab # List tabs +agent-browser tab new [url] # New tab +agent-browser tab 2 # Switch to tab +agent-browser tab close # Close tab +agent-browser window new # New window +``` + +### Frames + +```bash +agent-browser frame "#iframe" # Switch to iframe +agent-browser frame main # Back to main frame +``` + +### Dialogs + +```bash +agent-browser dialog accept [text] # Accept dialog +agent-browser dialog dismiss # Dismiss dialog +``` + +### JavaScript + +```bash +agent-browser eval "document.title" # Run JavaScript +``` + +### State management + +```bash +agent-browser state save auth.json # Save session state +agent-browser state load auth.json # Load saved state +``` + +## Example: Form submission + +```bash +agent-browser open https://example.com/form +agent-browser snapshot -i +# Output shows: textbox "Email" [ref=e1], textbox "Password" [ref=e2], button "Submit" [ref=e3] + +agent-browser fill @e1 "user@example.com" +agent-browser fill @e2 "password123" +agent-browser click @e3 +agent-browser wait --load networkidle +agent-browser snapshot -i # Check result +``` + +## Example: Authentication with saved state + +```bash +# Login once +agent-browser open https://app.example.com/login +agent-browser snapshot -i +agent-browser fill @e1 "username" +agent-browser fill @e2 "password" +agent-browser click @e3 +agent-browser wait --url "/dashboard" +agent-browser state save auth.json + +# Later sessions: load saved state +agent-browser state load auth.json +agent-browser open https://app.example.com/dashboard +``` + +## Sessions (parallel browsers) + +```bash +agent-browser --session test1 open site-a.com +agent-browser --session test2 open site-b.com +agent-browser session list +``` + +## JSON output (for parsing) + +Add `--json` for machine-readable output: + +```bash +agent-browser snapshot -i --json +agent-browser get text @e1 --json +``` + +## Debugging + +```bash +agent-browser open example.com --headed # Show browser window +agent-browser console # View console messages +agent-browser console --clear # Clear console +agent-browser errors # View page errors +agent-browser errors --clear # Clear errors +agent-browser highlight @e1 # Highlight element +agent-browser trace start # Start recording trace +agent-browser trace stop trace.zip # Stop and save trace +agent-browser record start ./debug.webm # Record from current page +agent-browser record stop # Save recording +agent-browser --cdp 9222 snapshot # Connect via CDP +``` + +## Troubleshooting + +- If the command is not found on Linux ARM64, use the full path in the bin folder. +- If an element is not found, use snapshot to find the correct ref. +- If the page is not loaded, add a wait command after navigation. +- Use --headed to see the browser window for debugging. + +## Options + +- --session uses an isolated session. +- --json provides JSON output. +- --full takes a full page screenshot. +- --headed shows the browser window. +- --timeout sets the command timeout in milliseconds. +- --cdp connects via Chrome DevTools Protocol. + +## Notes + +- Refs are stable per page load but change on navigation. +- Always snapshot after navigation to get new refs. +- Use fill instead of type for input fields to ensure existing text is cleared. + +## Reporting Issues + +- Skill issues: Open an issue at https://github.com/TheSethRose/Agent-Browser-CLI +- agent-browser CLI issues: Open an issue at https://github.com/vercel-labs/agent-browser diff --git a/skills/browser-use/.clawdhub/origin.json b/skills/browser-use/.clawdhub/origin.json new file mode 100644 index 0000000..f7adc75 --- /dev/null +++ b/skills/browser-use/.clawdhub/origin.json @@ -0,0 +1,7 @@ +{ + "version": 1, + "registry": "https://clawdhub.com", + "slug": "browser-use", + "installedVersion": "1.0.0", + "installedAt": 1769422454569 +} diff --git a/skills/browser-use/SKILL.md b/skills/browser-use/SKILL.md new file mode 100644 index 0000000..a6221b5 --- /dev/null +++ b/skills/browser-use/SKILL.md @@ -0,0 +1,162 @@ +--- +name: browser-use +description: Use Browser Use cloud API to spin up cloud browsers for Clawdbot and run autonomous browser tasks. Primary use is creating browser sessions with profiles (persisted logins/cookies) that Clawdbot can control. Secondary use is running task subagents for fast autonomous browser automation. Docs at docs.browser-use.com and docs.cloud.browser-use.com. +--- + +# Browser Use + +Browser Use provides cloud browsers and autonomous browser automation via API. + +**Docs:** +- Open source library: https://docs.browser-use.com +- Cloud API: https://docs.cloud.browser-use.com + +## Setup + +**API Key** is read from clawdbot config at `skills.entries.browser-use.apiKey`. + +If not configured, tell the user: +> To use Browser Use, you need an API key. Get one at https://cloud.browser-use.com (new signups get $10 free credit). Then configure it: +> ``` +> clawdbot config set skills.entries.browser-use.apiKey "bu_your_key_here" +> ``` + +Base URL: `https://api.browser-use.com/api/v2` + +All requests need header: `X-Browser-Use-API-Key: ` + +--- + +## 1. Browser Sessions (Primary) + +Spin up cloud browsers for Clawdbot to control directly. Use profiles to persist logins and cookies. + +### Create browser session + +```bash +# With profile (recommended - keeps you logged in) +curl -X POST "https://api.browser-use.com/api/v2/browsers" \ + -H "X-Browser-Use-API-Key: $API_KEY" \ + -H "Content-Type: application/json" \ + -d '{"profileId": "", "timeout": 60}' + +# Without profile (fresh browser) +curl -X POST "https://api.browser-use.com/api/v2/browsers" \ + -H "X-Browser-Use-API-Key: $API_KEY" \ + -H "Content-Type: application/json" \ + -d '{"timeout": 60}' +``` + +**Response:** +```json +{ + "id": "session-uuid", + "cdpUrl": "https://.cdp2.browser-use.com", + "liveUrl": "https://...", + "status": "active" +} +``` + +### Connect Clawdbot to the browser + +```bash +gateway config.patch '{"browser":{"profiles":{"browseruse":{"cdpUrl":""}}}}' +``` + +Now use the `browser` tool with `profile=browseruse` to control it. + +### List/stop browser sessions + +```bash +# List active sessions +curl "https://api.browser-use.com/api/v2/browsers" -H "X-Browser-Use-API-Key: $API_KEY" + +# Get session status +curl "https://api.browser-use.com/api/v2/browsers/" -H "X-Browser-Use-API-Key: $API_KEY" + +# Stop session (unused time is refunded) +curl -X PATCH "https://api.browser-use.com/api/v2/browsers/" \ + -H "X-Browser-Use-API-Key: $API_KEY" \ + -H "Content-Type: application/json" \ + -d '{"status": "stopped"}' +``` + +**Pricing:** $0.06/hour (Pay As You Go) or $0.03/hour (Business). Max 4 hours per session. Billed per minute, refunded for unused time. + +--- + +## 2. Profiles + +Profiles persist cookies and login state across browser sessions. Create one, log into your accounts in the browser, and reuse it. + +```bash +# List profiles +curl "https://api.browser-use.com/api/v2/profiles" -H "X-Browser-Use-API-Key: $API_KEY" + +# Create profile +curl -X POST "https://api.browser-use.com/api/v2/profiles" \ + -H "X-Browser-Use-API-Key: $API_KEY" \ + -H "Content-Type: application/json" \ + -d '{"name": "My Profile"}' + +# Delete profile +curl -X DELETE "https://api.browser-use.com/api/v2/profiles/" \ + -H "X-Browser-Use-API-Key: $API_KEY" +``` + +**Tip:** You can also sync cookies from your local Chrome using the Browser Use Chrome extension. + +--- + +## 3. Tasks (Subagent) + +Run autonomous browser tasks - like a subagent that handles browser interactions for you. Give it a prompt and it completes the task. + +**Always use `browser-use-llm`** - optimized for browser tasks, 3-5x faster than other models. + +```bash +curl -X POST "https://api.browser-use.com/api/v2/tasks" \ + -H "X-Browser-Use-API-Key: $API_KEY" \ + -H "Content-Type: application/json" \ + -d '{ + "task": "Go to amazon.com and find the price of the MacBook Air M3", + "llm": "browser-use-llm" + }' +``` + +### Poll for completion + +```bash +curl "https://api.browser-use.com/api/v2/tasks/" -H "X-Browser-Use-API-Key: $API_KEY" +``` + +**Response:** +```json +{ + "status": "finished", + "output": "The MacBook Air M3 is priced at $1,099", + "isSuccess": true, + "cost": "0.02" +} +``` + +Status values: `pending`, `running`, `finished`, `failed`, `stopped` + +### Task options + +| Option | Description | +|--------|-------------| +| `task` | Your prompt (required) | +| `llm` | Always use `browser-use-llm` | +| `startUrl` | Starting page | +| `maxSteps` | Max actions (default 100) | +| `sessionId` | Reuse existing session | +| `profileId` | Use a profile for auth | +| `flashMode` | Even faster execution | +| `vision` | Visual understanding | + +--- + +## Full API Reference + +See [references/api.md](references/api.md) for all endpoints including Sessions, Files, Skills, and Skills Marketplace.