[object Object]

← back to Videos Agentabrams Worker

initial scaffold (gitify-all 2026-05-06)

e82f4cbb76bdad7221906d296f2bc0c838da5926 · 2026-05-06 10:25:51 -0700 · Steve Abrams

Files touched

Diff

commit e82f4cbb76bdad7221906d296f2bc0c838da5926
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Wed May 6 10:25:51 2026 -0700

    initial scaffold (gitify-all 2026-05-06)
---
 .gitignore    |  12 +++
 cursor.txt    |   1 +
 flow-map.json |  26 +++++++
 log.jsonl     |  10 +++
 poll.py       | 237 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 5 files changed, 286 insertions(+)

diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..7e6a9c3
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,12 @@
+node_modules/
+.env
+.env.local
+.env.*.local
+.env.*
+tmp/
+*.log
+.DS_Store
+dist/
+build/
+.next/
+*.bak
diff --git a/cursor.txt b/cursor.txt
new file mode 100644
index 0000000..46c50e8
--- /dev/null
+++ b/cursor.txt
@@ -0,0 +1 @@
+2026-05-06T17:10:00.000Z
diff --git a/flow-map.json b/flow-map.json
new file mode 100644
index 0000000..01d2171
--- /dev/null
+++ b/flow-map.json
@@ -0,0 +1,26 @@
+{
+  "Ad-Signals-Rolodex": {
+    "flow_file": "Ad-Signals-Rolodex.json",
+    "url": "/Users/stevestudio2/Projects/ad-signals-rolodex/rolodex.html",
+    "app_name": "Ad-Signals-Rolodex",
+    "selectors_hint": "th (sortable column headers); .btn-pitch (per-row pitch button); #themeBtn (light/dark toggle); .row.tier-1 (Tier-A rows); .row.tier-2 (Tier-B); body (whole page); table tbody tr (any row)"
+  },
+  "Animals-Pitch": {
+    "flow_file": "Animals-Pitch.json",
+    "url": "/Users/stevestudio2/Projects/animals/exports/animals-sales-pitch-latest.html",
+    "app_name": "Animals-Pitch",
+    "selectors_hint": "th (sort columns); .btn-pitch (pitch button); #themeBtn (theme toggle); tr.tier-1, tr.tier1, tr.tier-A (Tier-A row variants); table tbody tr:first-child (top row); .filter-tier (tier filter)"
+  },
+  "Doctor-Pitch": {
+    "flow_file": "Doctor-Pitch.json",
+    "url": "/Users/stevestudio2/Projects/professional-directory/exports/doctor-sales-pitch-latest.html",
+    "app_name": "Doctor-Pitch",
+    "selectors_hint": "th; .btn-pitch; #themeBtn; tr.tier-1; table tbody tr:first-child"
+  },
+  "Ventura-Corridor": {
+    "flow_file": "Ventura-Corridor.json",
+    "url": "http://127.0.0.1:9780/",
+    "app_name": "Ventura-Corridor",
+    "selectors_hint": "#gs-input (search box); .gs-cell (any business tile); .b-3d (3D billboard); #stats (top counts); .theme-toggle-host (theme); #panel (detail panel); .b-tier (tier badge); .t.A, .t.B (tier-A and tier-B class)"
+  }
+}
diff --git a/log.jsonl b/log.jsonl
new file mode 100644
index 0000000..d85cc7a
--- /dev/null
+++ b/log.jsonl
@@ -0,0 +1,10 @@
+{"event": "tick", "new_count": 1, "cursor": "2026-05-06T17:10:00.000Z", "ts": "2026-05-06T17:11:05.906425+00:00"}
+{"event": "process_start", "request_ts": "2026-05-06T17:10:43.483Z", "video_ids": ["Animals-Pitch-2026-05-06.mp4"], "message": "add a hover step on Tier-A row before clicking the pitch button \u2014 make narration punchier", "ts": "2026-05-06T17:11:05.906722+00:00"}
+{"event": "ollama_error", "error": "timed out", "ts": "2026-05-06T17:14:05.911236+00:00"}
+{"event": "no_change", "video_id": "Animals-Pitch-2026-05-06.mp4", "ts": "2026-05-06T17:14:05.912156+00:00"}
+{"event": "tick", "new_count": 1, "cursor": "2026-05-06T17:10:00.000Z", "ts": "2026-05-06T17:14:24.832079+00:00"}
+{"event": "process_start", "request_ts": "2026-05-06T17:10:43.483Z", "video_ids": ["Animals-Pitch-2026-05-06.mp4"], "message": "add a hover step on Tier-A row before clicking the pitch button \u2014 make narration punchier", "ts": "2026-05-06T17:14:24.832254+00:00"}
+{"event": "ollama_error", "error": "timed out", "ts": "2026-05-06T17:20:24.837376+00:00"}
+{"event": "no_change", "video_id": "Animals-Pitch-2026-05-06.mp4", "ts": "2026-05-06T17:20:24.846187+00:00"}
+{"event": "tick", "new_count": 1, "cursor": "2026-05-06T17:10:00.000Z", "ts": "2026-05-06T17:21:27.115638+00:00"}
+{"event": "process_start", "request_ts": "2026-05-06T17:10:43.483Z", "video_ids": ["Animals-Pitch-2026-05-06.mp4"], "message": "add a hover step on Tier-A row before clicking the pitch button \u2014 make narration punchier", "ts": "2026-05-06T17:21:27.116457+00:00"}
diff --git a/poll.py b/poll.py
new file mode 100755
index 0000000..2d1a0a6
--- /dev/null
+++ b/poll.py
@@ -0,0 +1,237 @@
+#!/usr/bin/env python3
+"""
+videos.agentabrams.com chat → auto-regenerate videos.
+
+Polls /api/requests, processes new entries, edits flow JSON via local Ollama
+(qwen3:14b on Mac1), re-runs ship-video which re-publishes to Kamatera.
+
+Cursor (last-processed timestamp) lives at cursor.txt to avoid re-processing.
+First run starts cursor at "now" so legacy/test entries are skipped.
+"""
+import os, sys, json, time, base64, subprocess, urllib.request, urllib.error, re
+from pathlib import Path
+from datetime import datetime, timezone
+
+ROOT = Path.home() / 'Projects' / 'videos-agentabrams-worker'
+ROOT.mkdir(parents=True, exist_ok=True)
+CURSOR = ROOT / 'cursor.txt'
+LOG = ROOT / 'log.jsonl'
+MAP_FILE = ROOT / 'flow-map.json'
+FLOWS_DIR = Path.home() / '.claude' / 'skills' / 'app-demo-video' / 'flows'
+SHIP_VIDEO = Path.home() / 'bin' / 'ship-video'
+OLLAMA_URL = os.environ.get('OLLAMA_URL', 'http://192.168.1.133:11434')
+OLLAMA_MODEL = os.environ.get('OLLAMA_MODEL', 'qwen3:14b')
+
+API_BASE = 'https://videos.agentabrams.com'
+API_AUTH = ('admin', 'DWSecure2024!')
+
+
+def log_event(entry):
+    entry['ts'] = datetime.now(timezone.utc).isoformat()
+    with LOG.open('a') as f:
+        f.write(json.dumps(entry) + '\n')
+
+
+def http_get(url):
+    req = urllib.request.Request(url)
+    token = base64.b64encode(f'{API_AUTH[0]}:{API_AUTH[1]}'.encode()).decode()
+    req.add_header('Authorization', f'Basic {token}')
+    with urllib.request.urlopen(req, timeout=30) as r:
+        return json.loads(r.read())
+
+
+def call_ollama(system, user, timeout=360):
+    # /no_think disables qwen3 reasoning tokens (massive speedup, otherwise hangs on long prompts)
+    body = json.dumps({
+        'model': OLLAMA_MODEL,
+        'system': system,
+        'prompt': '/no_think\n\n' + user,
+        'stream': False,
+        'format': 'json',
+        'options': {'temperature': 0.4, 'num_ctx': 8192, 'num_predict': 2048},
+    }).encode()
+    req = urllib.request.Request(
+        f'{OLLAMA_URL}/api/generate',
+        data=body,
+        headers={'Content-Type': 'application/json'},
+    )
+    with urllib.request.urlopen(req, timeout=timeout) as r:
+        return json.loads(r.read())['response']
+
+
+def vid_id_to_slug(vid_id):
+    """'Animals-Pitch-2026-05-06.mp4' → 'Animals-Pitch'"""
+    base = vid_id[:-4] if vid_id.endswith('.mp4') else vid_id
+    return re.sub(r'-\d{4}-\d{2}-\d{2}$', '', base)
+
+
+def get_cursor():
+    if CURSOR.exists():
+        return CURSOR.read_text().strip()
+    now = datetime.now(timezone.utc).isoformat()
+    CURSOR.write_text(now)
+    return now
+
+
+def set_cursor(ts):
+    CURSOR.write_text(ts)
+
+
+VALID_ACTIONS = {'wait', 'scroll', 'click', 'hover'}
+
+
+def validate_flow(flow):
+    if not isinstance(flow, dict): return False, 'not a dict'
+    if 'steps' not in flow or not isinstance(flow['steps'], list): return False, 'no steps[]'
+    if not flow['steps']: return False, 'empty steps'
+    if len(flow['steps']) > 16: return False, f'too many steps ({len(flow["steps"])})'
+    for i, s in enumerate(flow['steps']):
+        if not isinstance(s, dict): return False, f'step {i} not dict'
+        if s.get('action') not in VALID_ACTIONS:
+            return False, f'step {i} invalid action: {s.get("action")}'
+        if s['action'] in ('click', 'hover') and not s.get('selector'):
+            return False, f'step {i} ({s["action"]}) missing selector'
+    return True, 'ok'
+
+
+def edit_flow_via_llm(current_flow, user_msg, entry):
+    system = (
+        "You edit JSON flow files for a web-app demo video recorder.\n\n"
+        "Schema: { \"steps\": [ ... ] }\n\n"
+        "Action shapes:\n"
+        "  {\"action\":\"wait\",\"ms\":<int 500-3000>,\"narration\":\"<5-10 words>\"}\n"
+        "  {\"action\":\"scroll\",\"y\":<int>,\"duration\":<ms>,\"narration\":\"<5-10 words>\"}\n"
+        "  {\"action\":\"click\",\"selector\":\"<css>\",\"narration\":\"<5-10 words>\"}\n"
+        "  {\"action\":\"hover\",\"selector\":\"<css>\",\"narration\":\"<5-10 words>\"}\n\n"
+        "Rules:\n"
+        "- Use ONLY the four action types above. There is no \"type\" action.\n"
+        "- Selectors must be valid CSS, ideally drawn from the provided selector hints.\n"
+        "- Narration: short, punchy, 5-10 words, sales-pitch tone.\n"
+        "- Total steps must be ≥6 and ≤14.\n"
+        "- Output VALID JSON object only. NO commentary, NO markdown fences."
+    )
+    user = (
+        f"Current flow JSON:\n{json.dumps(current_flow, indent=2)}\n\n"
+        f"App: {entry['app_name']}\n"
+        f"URL: {entry['url']}\n"
+        f"Available selectors / hints: {entry.get('selectors_hint', '(none)')}\n\n"
+        f"User request: \"{user_msg}\"\n\n"
+        "Output the COMPLETE updated flow JSON. Be creative — change narration tone, "
+        "add hovers, reorder, swap selectors — whatever the user is asking for. If the "
+        "request is ambiguous, make your best guess. Return only the JSON."
+    )
+    try:
+        resp = call_ollama(system, user)
+    except Exception as e:
+        log_event({'event': 'ollama_error', 'error': str(e)})
+        return None
+
+    new_flow = None
+    try:
+        new_flow = json.loads(resp)
+    except json.JSONDecodeError:
+        m = re.search(r'\{[\s\S]*\}', resp)
+        if m:
+            try:
+                new_flow = json.loads(m.group(0))
+            except json.JSONDecodeError:
+                pass
+    if new_flow is None:
+        log_event({'event': 'llm_invalid_json', 'response_head': resp[:400]})
+        return None
+
+    ok, why = validate_flow(new_flow)
+    if not ok:
+        log_event({'event': 'llm_invalid_schema', 'reason': why, 'flow': new_flow})
+        return None
+
+    if new_flow == current_flow:
+        return None
+    return new_flow
+
+
+def process_request(req, flow_map):
+    msg = req.get('message', '')
+    ids = req.get('video_ids', []) or []
+    log_event({'event': 'process_start', 'request_ts': req['ts'], 'video_ids': ids, 'message': msg})
+
+    for vid_id in ids:
+        slug = vid_id_to_slug(vid_id)
+        if slug not in flow_map:
+            log_event({'event': 'unknown_slug', 'video_id': vid_id, 'slug': slug})
+            continue
+        entry = flow_map[slug]
+        flow_path = FLOWS_DIR / entry['flow_file']
+        if not flow_path.exists():
+            log_event({'event': 'flow_file_missing', 'video_id': vid_id, 'path': str(flow_path)})
+            continue
+
+        current = json.loads(flow_path.read_text())
+        new_flow = edit_flow_via_llm(current, msg, entry)
+        if not new_flow:
+            log_event({'event': 'no_change', 'video_id': vid_id})
+            continue
+
+        # Backup + write
+        backup = flow_path.with_suffix(f'.{int(time.time())}.bak.json')
+        backup.write_text(flow_path.read_text())
+        flow_path.write_text(json.dumps(new_flow, indent=2) + '\n')
+        log_event({
+            'event': 'flow_updated',
+            'video_id': vid_id,
+            'flow_file': str(flow_path),
+            'backup': str(backup),
+            'old_steps': len(current.get('steps', [])),
+            'new_steps': len(new_flow.get('steps', [])),
+        })
+
+        # Re-record
+        env = os.environ.copy()
+        try:
+            result = subprocess.run(
+                [str(SHIP_VIDEO), entry['app_name'], entry['url']],
+                capture_output=True, text=True, env=env, timeout=300,
+            )
+            log_event({
+                'event': 'ship_video_done',
+                'video_id': vid_id,
+                'app_name': entry['app_name'],
+                'returncode': result.returncode,
+                'stdout_tail': (result.stdout or '')[-500:],
+                'stderr_tail': (result.stderr or '')[-500:],
+            })
+        except subprocess.TimeoutExpired:
+            log_event({'event': 'ship_video_timeout', 'video_id': vid_id})
+
+
+def main():
+    if not MAP_FILE.exists():
+        print(f'flow-map.json missing at {MAP_FILE}', file=sys.stderr)
+        sys.exit(2)
+    flow_map = json.loads(MAP_FILE.read_text())
+
+    cursor = get_cursor()
+    try:
+        data = http_get(f'{API_BASE}/api/requests')
+    except Exception as e:
+        log_event({'event': 'fetch_failed', 'error': str(e)})
+        return
+
+    requests = data.get('requests', [])
+    new = [r for r in requests if r.get('ts', '') > cursor]
+    if not new:
+        return
+
+    new.sort(key=lambda r: r['ts'])  # oldest first
+    log_event({'event': 'tick', 'new_count': len(new), 'cursor': cursor})
+
+    for req in new:
+        try:
+            process_request(req, flow_map)
+        except Exception as e:
+            log_event({'event': 'process_exception', 'request_ts': req['ts'], 'error': str(e)})
+        set_cursor(req['ts'])
+
+
+if __name__ == '__main__':
+    main()

(oldest)  ·  back to Videos Agentabrams Worker  ·  ignore runtime files (cursor, logs) 170bcf7 →