eyeroll 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- eyeroll-0.1.0/.env.example +1 -0
- eyeroll-0.1.0/.github/workflows/ci.yml +68 -0
- eyeroll-0.1.0/.github/workflows/publish-pypi.yml +52 -0
- eyeroll-0.1.0/.gitignore +67 -0
- eyeroll-0.1.0/CLAUDE.md +46 -0
- eyeroll-0.1.0/LICENSE +21 -0
- eyeroll-0.1.0/PKG-INFO +130 -0
- eyeroll-0.1.0/README.md +99 -0
- eyeroll-0.1.0/eyeroll/__init__.py +1 -0
- eyeroll-0.1.0/eyeroll/acquire.py +127 -0
- eyeroll-0.1.0/eyeroll/analyze.py +318 -0
- eyeroll-0.1.0/eyeroll/cli.py +112 -0
- eyeroll-0.1.0/eyeroll/extract.py +151 -0
- eyeroll-0.1.0/eyeroll/watch.py +174 -0
- eyeroll-0.1.0/pyproject.toml +54 -0
- eyeroll-0.1.0/skills/video-to-pr/SKILL.md +114 -0
- eyeroll-0.1.0/skills/video-to-skill/SKILL.md +137 -0
- eyeroll-0.1.0/skills/watch-video/SKILL.md +123 -0
- eyeroll-0.1.0/tests/__init__.py +0 -0
- eyeroll-0.1.0/tests/test_acquire.py +25 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
GEMINI_API_KEY=your-api-key-here
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches:
|
|
6
|
+
- main
|
|
7
|
+
- feat/**
|
|
8
|
+
pull_request:
|
|
9
|
+
|
|
10
|
+
jobs:
|
|
11
|
+
test:
|
|
12
|
+
runs-on: ubuntu-latest
|
|
13
|
+
strategy:
|
|
14
|
+
fail-fast: false
|
|
15
|
+
matrix:
|
|
16
|
+
python-version: ["3.11", "3.12", "3.13"]
|
|
17
|
+
|
|
18
|
+
steps:
|
|
19
|
+
- name: Checkout
|
|
20
|
+
uses: actions/checkout@v5
|
|
21
|
+
|
|
22
|
+
- name: Set up Python
|
|
23
|
+
uses: actions/setup-python@v6
|
|
24
|
+
with:
|
|
25
|
+
python-version: ${{ matrix.python-version }}
|
|
26
|
+
|
|
27
|
+
- name: Install dependencies
|
|
28
|
+
run: |
|
|
29
|
+
python -m pip install --upgrade pip
|
|
30
|
+
pip install -e '.[dev]'
|
|
31
|
+
|
|
32
|
+
- name: Run tests
|
|
33
|
+
run: python -m pytest -q
|
|
34
|
+
|
|
35
|
+
publish:
|
|
36
|
+
needs: test
|
|
37
|
+
runs-on: ubuntu-latest
|
|
38
|
+
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
|
|
39
|
+
environment: pypi
|
|
40
|
+
permissions:
|
|
41
|
+
id-token: write
|
|
42
|
+
steps:
|
|
43
|
+
- name: Checkout
|
|
44
|
+
uses: actions/checkout@v5
|
|
45
|
+
|
|
46
|
+
- name: Set up Python
|
|
47
|
+
uses: actions/setup-python@v6
|
|
48
|
+
with:
|
|
49
|
+
python-version: "3.12"
|
|
50
|
+
|
|
51
|
+
- name: Build
|
|
52
|
+
run: |
|
|
53
|
+
python -m pip install --upgrade pip build
|
|
54
|
+
python -m build
|
|
55
|
+
|
|
56
|
+
- name: Check if version exists on PyPI
|
|
57
|
+
id: check
|
|
58
|
+
run: |
|
|
59
|
+
VERSION=$(python -c "import tomllib; print(tomllib.load(open('pyproject.toml','rb'))['project']['version'])")
|
|
60
|
+
if pip index versions eyeroll 2>/dev/null | grep -q "$VERSION"; then
|
|
61
|
+
echo "exists=true" >> "$GITHUB_OUTPUT"
|
|
62
|
+
else
|
|
63
|
+
echo "exists=false" >> "$GITHUB_OUTPUT"
|
|
64
|
+
fi
|
|
65
|
+
|
|
66
|
+
- name: Publish to PyPI
|
|
67
|
+
if: steps.check.outputs.exists == 'false'
|
|
68
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
name: Publish Python Package
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
release:
|
|
5
|
+
types: [published]
|
|
6
|
+
|
|
7
|
+
permissions:
|
|
8
|
+
contents: read
|
|
9
|
+
|
|
10
|
+
jobs:
|
|
11
|
+
release-build:
|
|
12
|
+
runs-on: ubuntu-latest
|
|
13
|
+
|
|
14
|
+
steps:
|
|
15
|
+
- uses: actions/checkout@v5
|
|
16
|
+
|
|
17
|
+
- uses: actions/setup-python@v6
|
|
18
|
+
with:
|
|
19
|
+
python-version: "3.x"
|
|
20
|
+
|
|
21
|
+
- name: Build release distributions
|
|
22
|
+
run: |
|
|
23
|
+
python -m pip install --upgrade pip build
|
|
24
|
+
python -m build
|
|
25
|
+
|
|
26
|
+
- name: Upload distributions
|
|
27
|
+
uses: actions/upload-artifact@v6
|
|
28
|
+
with:
|
|
29
|
+
name: release-dists
|
|
30
|
+
path: dist/
|
|
31
|
+
|
|
32
|
+
pypi-publish:
|
|
33
|
+
runs-on: ubuntu-latest
|
|
34
|
+
needs: release-build
|
|
35
|
+
permissions:
|
|
36
|
+
id-token: write
|
|
37
|
+
contents: read
|
|
38
|
+
environment:
|
|
39
|
+
name: pypi
|
|
40
|
+
url: https://pypi.org/project/eyeroll/
|
|
41
|
+
|
|
42
|
+
steps:
|
|
43
|
+
- name: Retrieve release distributions
|
|
44
|
+
uses: actions/download-artifact@v5
|
|
45
|
+
with:
|
|
46
|
+
name: release-dists
|
|
47
|
+
path: dist/
|
|
48
|
+
|
|
49
|
+
- name: Publish release distributions to PyPI
|
|
50
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
51
|
+
with:
|
|
52
|
+
packages-dir: dist/
|
eyeroll-0.1.0/.gitignore
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
*.egg-info/
|
|
6
|
+
dist/
|
|
7
|
+
build/
|
|
8
|
+
*.egg
|
|
9
|
+
|
|
10
|
+
# Environment
|
|
11
|
+
.env
|
|
12
|
+
.venv/
|
|
13
|
+
venv/
|
|
14
|
+
|
|
15
|
+
# IDE
|
|
16
|
+
.idea/
|
|
17
|
+
.vscode/
|
|
18
|
+
*.swp
|
|
19
|
+
*.swo
|
|
20
|
+
|
|
21
|
+
# OS
|
|
22
|
+
.DS_Store
|
|
23
|
+
Thumbs.db
|
|
24
|
+
|
|
25
|
+
# Project
|
|
26
|
+
*.mp4
|
|
27
|
+
*.webm
|
|
28
|
+
*.gif
|
|
29
|
+
*.png
|
|
30
|
+
*.jpg
|
|
31
|
+
*.jpeg
|
|
32
|
+
!docs/*.png
|
|
33
|
+
output/
|
|
34
|
+
tmp/
|
|
35
|
+
.eyeroll/
|
|
36
|
+
|
|
37
|
+
# npx skills install artifacts (local symlinks)
|
|
38
|
+
.agents/
|
|
39
|
+
.adal/
|
|
40
|
+
.augment/
|
|
41
|
+
.claude/
|
|
42
|
+
.codebuddy/
|
|
43
|
+
.commandcode/
|
|
44
|
+
.continue/
|
|
45
|
+
.crush/
|
|
46
|
+
.cursor/
|
|
47
|
+
.factory/
|
|
48
|
+
.goose/
|
|
49
|
+
.iflow/
|
|
50
|
+
.junie/
|
|
51
|
+
.kilocode/
|
|
52
|
+
.kiro/
|
|
53
|
+
.kode/
|
|
54
|
+
.mcpjam/
|
|
55
|
+
.mux/
|
|
56
|
+
.neovate/
|
|
57
|
+
.openhands/
|
|
58
|
+
.pi/
|
|
59
|
+
.pochi/
|
|
60
|
+
.qoder/
|
|
61
|
+
.qwen/
|
|
62
|
+
.roo/
|
|
63
|
+
.trae/
|
|
64
|
+
.vibe/
|
|
65
|
+
.windsurf/
|
|
66
|
+
.zencoder/
|
|
67
|
+
skills-lock.json
|
eyeroll-0.1.0/CLAUDE.md
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# CLAUDE.md
|
|
2
|
+
|
|
3
|
+
## Project
|
|
4
|
+
|
|
5
|
+
eyeroll — AI eyes that roll through video footage. Takes video URLs (Loom, YouTube), local video files, or screenshots as input. Uses Gemini Flash for vision analysis and audio transcription. Produces structured notes that coding agents can act on — fix bugs, build features, create skills, or anything else.
|
|
6
|
+
|
|
7
|
+
## Commands
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
# Install
|
|
11
|
+
pip install .
|
|
12
|
+
# or with uv:
|
|
13
|
+
uv sync
|
|
14
|
+
|
|
15
|
+
# Run tests
|
|
16
|
+
pytest
|
|
17
|
+
pytest --cov --cov-report=term-missing
|
|
18
|
+
|
|
19
|
+
# CLI
|
|
20
|
+
eyeroll init # set up Gemini API key
|
|
21
|
+
eyeroll watch <url-or-path> # analyze a video/screenshot
|
|
22
|
+
eyeroll watch <url> --context "broken after PR #432" # with reporter context
|
|
23
|
+
eyeroll watch <path> --verbose --output report.md # verbose + write to file
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Architecture
|
|
27
|
+
|
|
28
|
+
- **Pipeline**: `acquire.py` (download/locate) → `extract.py` (frames + audio) → `analyze.py` (Gemini vision + synthesis) → `watch.py` (orchestrator)
|
|
29
|
+
- **acquire.py**: Downloads from URLs via yt-dlp, resolves local files. Returns file_path, media_type, title.
|
|
30
|
+
- **extract.py**: ffmpeg wrappers for key frame extraction, audio extraction, duration detection. Falls back to imageio-ffmpeg.
|
|
31
|
+
- **analyze.py**: Gemini Flash API calls. Frame-by-frame analysis with structured prompts, direct video upload for short videos, audio transcription, and synthesis into bug report.
|
|
32
|
+
- **watch.py**: Orchestrates the full pipeline. Chooses strategy (direct upload vs frame-by-frame) based on video size/duration. Handles cleanup.
|
|
33
|
+
- **cli.py**: Click CLI with `init` and `watch` commands.
|
|
34
|
+
|
|
35
|
+
## Key design decisions
|
|
36
|
+
|
|
37
|
+
- **Single backend (Gemini Flash)**: Intentionally simple. One API key, one set of failure modes. Gemini handles video, images, and audio natively.
|
|
38
|
+
- **Direct upload vs frame-by-frame**: Videos under 20MB/2min are sent whole to Gemini (better context). Longer videos use frame extraction.
|
|
39
|
+
- **Context text is critical**: Most bug videos are silent screen recordings. The reporter's Slack message or issue description often contains more intent than the video itself.
|
|
40
|
+
- **No codebase mapping in the skill itself**: The skill produces a bug report. Claude Code (which has codebase context) handles finding relevant code and implementing fixes.
|
|
41
|
+
|
|
42
|
+
## Testing patterns
|
|
43
|
+
|
|
44
|
+
- Mock all Gemini API calls in tests — never hit external services.
|
|
45
|
+
- Use synthetic test videos generated via ffmpeg fixtures.
|
|
46
|
+
- Test acquire.py with both URL and local file paths.
|
eyeroll-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 mnvsk97
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
eyeroll-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: eyeroll
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: AI eyes that roll through video footage — watch, understand, turn into code actions.
|
|
5
|
+
Project-URL: Homepage, https://github.com/mnvsk97/eyeroll
|
|
6
|
+
Project-URL: Repository, https://github.com/mnvsk97/eyeroll
|
|
7
|
+
Project-URL: Issues, https://github.com/mnvsk97/eyeroll/issues
|
|
8
|
+
Author: mnvsk97
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: agent,ai,bug-report,claude-code,gemini,skills,video
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Topic :: Multimedia :: Video
|
|
20
|
+
Classifier: Topic :: Software Development :: Bug Tracking
|
|
21
|
+
Requires-Python: >=3.11
|
|
22
|
+
Requires-Dist: click>=8.1
|
|
23
|
+
Requires-Dist: google-genai>=1.0
|
|
24
|
+
Requires-Dist: imageio-ffmpeg>=0.5
|
|
25
|
+
Requires-Dist: python-dotenv>=1.0
|
|
26
|
+
Requires-Dist: yt-dlp>=2024.0
|
|
27
|
+
Provides-Extra: dev
|
|
28
|
+
Requires-Dist: pytest-cov>=4.0; extra == 'dev'
|
|
29
|
+
Requires-Dist: pytest>=7.0; extra == 'dev'
|
|
30
|
+
Description-Content-Type: text/markdown
|
|
31
|
+
|
|
32
|
+
# eyeroll
|
|
33
|
+
|
|
34
|
+
AI eyes that roll through video footage — watch, understand, act.
|
|
35
|
+
|
|
36
|
+
eyeroll is a collection of AI coding agent skills that analyze screen recordings, Loom videos, YouTube links, and screenshots, then turn them into code actions.
|
|
37
|
+
|
|
38
|
+
## Skills
|
|
39
|
+
|
|
40
|
+
| Skill | What it does |
|
|
41
|
+
|-------|-------------|
|
|
42
|
+
| **watch-video** | Watch a video and produce structured notes |
|
|
43
|
+
| **video-to-pr** | Watch a bug video → diagnose → fix → raise PR |
|
|
44
|
+
| **video-to-skill** | Watch a tutorial/demo → generate a Claude Code skill |
|
|
45
|
+
|
|
46
|
+
## Install
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
# Install all skills
|
|
50
|
+
npx skills add mnvsk97/eyeroll
|
|
51
|
+
|
|
52
|
+
# Install a specific skill
|
|
53
|
+
npx skills add mnvsk97/eyeroll@video-to-pr
|
|
54
|
+
npx skills add mnvsk97/eyeroll@video-to-skill
|
|
55
|
+
npx skills add mnvsk97/eyeroll@watch-video
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Setup
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
pip install eyeroll # or: pip install git+https://github.com/mnvsk97/eyeroll.git
|
|
62
|
+
eyeroll init # set up Gemini API key
|
|
63
|
+
brew install yt-dlp # for URL downloads (Loom, YouTube)
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Or just set the key: `export GEMINI_API_KEY=your-key` ([get one free](https://aistudio.google.com/apikey))
|
|
67
|
+
|
|
68
|
+
## Usage
|
|
69
|
+
|
|
70
|
+
### In Claude Code (via skills)
|
|
71
|
+
|
|
72
|
+
```
|
|
73
|
+
User: fix this bug: https://loom.com/share/abc123
|
|
74
|
+
→ video-to-pr skill activates, watches video, finds code, fixes, raises PR
|
|
75
|
+
|
|
76
|
+
User: watch this tutorial and create a skill from it: ./demo.mp4
|
|
77
|
+
→ video-to-skill skill activates, watches video, generates SKILL.md
|
|
78
|
+
|
|
79
|
+
User: look at this recording, what's going on?
|
|
80
|
+
→ watch-video skill activates, produces structured notes
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### Standalone CLI
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
eyeroll watch https://loom.com/share/abc123
|
|
87
|
+
eyeroll watch ./bug.mp4 --context "checkout broken after PR #432"
|
|
88
|
+
eyeroll watch screenshot.png --verbose
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## How it works
|
|
92
|
+
|
|
93
|
+
```
|
|
94
|
+
Video (Loom / YouTube / local file / screenshot)
|
|
95
|
+
↓
|
|
96
|
+
eyeroll extracts: frames, audio, on-screen text
|
|
97
|
+
↓
|
|
98
|
+
Gemini Flash analyzes: what's shown, what's said, what happened
|
|
99
|
+
↓
|
|
100
|
+
Structured notes → skill decides what to do next
|
|
101
|
+
↓
|
|
102
|
+
video-to-pr: search codebase → fix → PR
|
|
103
|
+
video-to-skill: extract workflow → generate SKILL.md
|
|
104
|
+
watch-video: return notes to the agent
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## Supported inputs
|
|
108
|
+
|
|
109
|
+
| Input | Notes |
|
|
110
|
+
|-------|-------|
|
|
111
|
+
| Loom URLs | Requires yt-dlp |
|
|
112
|
+
| YouTube URLs | Requires yt-dlp |
|
|
113
|
+
| Local video files (.mp4, .webm, .mov) | Direct analysis |
|
|
114
|
+
| Screenshots (.png, .jpg, .gif) | Single-frame analysis |
|
|
115
|
+
| Any yt-dlp supported URL | 1000+ sites |
|
|
116
|
+
|
|
117
|
+
## Requirements
|
|
118
|
+
|
|
119
|
+
- Python 3.11+
|
|
120
|
+
- ffmpeg (`brew install ffmpeg`)
|
|
121
|
+
- yt-dlp (`brew install yt-dlp`) — for URL downloads
|
|
122
|
+
- Gemini API key ([free](https://aistudio.google.com/apikey))
|
|
123
|
+
|
|
124
|
+
## Cost
|
|
125
|
+
|
|
126
|
+
Typically under $0.15 per video analysis using Gemini 2.0 Flash.
|
|
127
|
+
|
|
128
|
+
## License
|
|
129
|
+
|
|
130
|
+
MIT
|
eyeroll-0.1.0/README.md
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# eyeroll
|
|
2
|
+
|
|
3
|
+
AI eyes that roll through video footage — watch, understand, act.
|
|
4
|
+
|
|
5
|
+
eyeroll is a collection of AI coding agent skills that analyze screen recordings, Loom videos, YouTube links, and screenshots, then turn them into code actions.
|
|
6
|
+
|
|
7
|
+
## Skills
|
|
8
|
+
|
|
9
|
+
| Skill | What it does |
|
|
10
|
+
|-------|-------------|
|
|
11
|
+
| **watch-video** | Watch a video and produce structured notes |
|
|
12
|
+
| **video-to-pr** | Watch a bug video → diagnose → fix → raise PR |
|
|
13
|
+
| **video-to-skill** | Watch a tutorial/demo → generate a Claude Code skill |
|
|
14
|
+
|
|
15
|
+
## Install
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
# Install all skills
|
|
19
|
+
npx skills add mnvsk97/eyeroll
|
|
20
|
+
|
|
21
|
+
# Install a specific skill
|
|
22
|
+
npx skills add mnvsk97/eyeroll@video-to-pr
|
|
23
|
+
npx skills add mnvsk97/eyeroll@video-to-skill
|
|
24
|
+
npx skills add mnvsk97/eyeroll@watch-video
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Setup
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
pip install eyeroll # or: pip install git+https://github.com/mnvsk97/eyeroll.git
|
|
31
|
+
eyeroll init # set up Gemini API key
|
|
32
|
+
brew install yt-dlp # for URL downloads (Loom, YouTube)
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Or just set the key: `export GEMINI_API_KEY=your-key` ([get one free](https://aistudio.google.com/apikey))
|
|
36
|
+
|
|
37
|
+
## Usage
|
|
38
|
+
|
|
39
|
+
### In Claude Code (via skills)
|
|
40
|
+
|
|
41
|
+
```
|
|
42
|
+
User: fix this bug: https://loom.com/share/abc123
|
|
43
|
+
→ video-to-pr skill activates, watches video, finds code, fixes, raises PR
|
|
44
|
+
|
|
45
|
+
User: watch this tutorial and create a skill from it: ./demo.mp4
|
|
46
|
+
→ video-to-skill skill activates, watches video, generates SKILL.md
|
|
47
|
+
|
|
48
|
+
User: look at this recording, what's going on?
|
|
49
|
+
→ watch-video skill activates, produces structured notes
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### Standalone CLI
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
eyeroll watch https://loom.com/share/abc123
|
|
56
|
+
eyeroll watch ./bug.mp4 --context "checkout broken after PR #432"
|
|
57
|
+
eyeroll watch screenshot.png --verbose
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## How it works
|
|
61
|
+
|
|
62
|
+
```
|
|
63
|
+
Video (Loom / YouTube / local file / screenshot)
|
|
64
|
+
↓
|
|
65
|
+
eyeroll extracts: frames, audio, on-screen text
|
|
66
|
+
↓
|
|
67
|
+
Gemini Flash analyzes: what's shown, what's said, what happened
|
|
68
|
+
↓
|
|
69
|
+
Structured notes → skill decides what to do next
|
|
70
|
+
↓
|
|
71
|
+
video-to-pr: search codebase → fix → PR
|
|
72
|
+
video-to-skill: extract workflow → generate SKILL.md
|
|
73
|
+
watch-video: return notes to the agent
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Supported inputs
|
|
77
|
+
|
|
78
|
+
| Input | Notes |
|
|
79
|
+
|-------|-------|
|
|
80
|
+
| Loom URLs | Requires yt-dlp |
|
|
81
|
+
| YouTube URLs | Requires yt-dlp |
|
|
82
|
+
| Local video files (.mp4, .webm, .mov) | Direct analysis |
|
|
83
|
+
| Screenshots (.png, .jpg, .gif) | Single-frame analysis |
|
|
84
|
+
| Any yt-dlp supported URL | 1000+ sites |
|
|
85
|
+
|
|
86
|
+
## Requirements
|
|
87
|
+
|
|
88
|
+
- Python 3.11+
|
|
89
|
+
- ffmpeg (`brew install ffmpeg`)
|
|
90
|
+
- yt-dlp (`brew install yt-dlp`) — for URL downloads
|
|
91
|
+
- Gemini API key ([free](https://aistudio.google.com/apikey))
|
|
92
|
+
|
|
93
|
+
## Cost
|
|
94
|
+
|
|
95
|
+
Typically under $0.15 per video analysis using Gemini 2.0 Flash.
|
|
96
|
+
|
|
97
|
+
## License
|
|
98
|
+
|
|
99
|
+
MIT
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""eyeroll — AI eyes that roll through video footage. Watch, understand, act."""
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"""Download or locate video/image from URL or local path."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import shutil
|
|
5
|
+
import subprocess
|
|
6
|
+
import tempfile
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
SUPPORTED_VIDEO_EXTS = {".mp4", ".webm", ".mov", ".avi", ".mkv"}
|
|
10
|
+
SUPPORTED_IMAGE_EXTS = {".png", ".jpg", ".jpeg", ".gif", ".webp", ".bmp"}
|
|
11
|
+
SUPPORTED_EXTS = SUPPORTED_VIDEO_EXTS | SUPPORTED_IMAGE_EXTS
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _is_url(source: str) -> bool:
|
|
15
|
+
return source.startswith("http://") or source.startswith("https://")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _get_ytdlp() -> str:
|
|
19
|
+
path = shutil.which("yt-dlp")
|
|
20
|
+
if path:
|
|
21
|
+
return path
|
|
22
|
+
raise RuntimeError(
|
|
23
|
+
"yt-dlp is not installed.\n\n"
|
|
24
|
+
"Install with: brew install yt-dlp (macOS)\n"
|
|
25
|
+
" pip install yt-dlp (any platform)"
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def detect_media_type(file_path: str) -> str:
|
|
30
|
+
"""Return 'video' or 'image' based on file extension."""
|
|
31
|
+
ext = Path(file_path).suffix.lower()
|
|
32
|
+
if ext in SUPPORTED_VIDEO_EXTS:
|
|
33
|
+
return "video"
|
|
34
|
+
if ext in SUPPORTED_IMAGE_EXTS:
|
|
35
|
+
return "image"
|
|
36
|
+
raise ValueError(f"Unsupported file type: {ext}")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def acquire(source: str, output_dir: str | None = None) -> dict:
|
|
40
|
+
"""Download or locate media from a URL or local path.
|
|
41
|
+
|
|
42
|
+
Returns dict with keys:
|
|
43
|
+
file_path: absolute path to the media file
|
|
44
|
+
media_type: 'video' or 'image'
|
|
45
|
+
source_url: original URL (or None for local files)
|
|
46
|
+
title: video title (from yt-dlp metadata, or filename)
|
|
47
|
+
"""
|
|
48
|
+
if _is_url(source):
|
|
49
|
+
return _download_url(source, output_dir)
|
|
50
|
+
return _resolve_local(source)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _download_url(url: str, output_dir: str | None = None) -> dict:
|
|
54
|
+
ytdlp = _get_ytdlp()
|
|
55
|
+
if output_dir is None:
|
|
56
|
+
output_dir = tempfile.mkdtemp(prefix="eyeroll_")
|
|
57
|
+
|
|
58
|
+
# First, get metadata to know the title
|
|
59
|
+
meta_result = subprocess.run(
|
|
60
|
+
[ytdlp, "--dump-json", "--no-download", url],
|
|
61
|
+
capture_output=True,
|
|
62
|
+
text=True,
|
|
63
|
+
timeout=30,
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
title = None
|
|
67
|
+
if meta_result.returncode == 0:
|
|
68
|
+
import json
|
|
69
|
+
try:
|
|
70
|
+
meta = json.loads(meta_result.stdout)
|
|
71
|
+
title = meta.get("title")
|
|
72
|
+
except json.JSONDecodeError:
|
|
73
|
+
pass
|
|
74
|
+
|
|
75
|
+
# Download the video
|
|
76
|
+
output_template = os.path.join(output_dir, "%(title)s.%(ext)s")
|
|
77
|
+
result = subprocess.run(
|
|
78
|
+
[
|
|
79
|
+
ytdlp,
|
|
80
|
+
"--no-playlist",
|
|
81
|
+
"--merge-output-format", "mp4",
|
|
82
|
+
"-o", output_template,
|
|
83
|
+
url,
|
|
84
|
+
],
|
|
85
|
+
capture_output=True,
|
|
86
|
+
text=True,
|
|
87
|
+
timeout=300,
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
if result.returncode != 0:
|
|
91
|
+
raise RuntimeError(
|
|
92
|
+
f"yt-dlp failed to download: {url}\n\n"
|
|
93
|
+
f"stderr: {result.stderr[:500]}"
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
# Find the downloaded file
|
|
97
|
+
downloaded = _find_media_file(output_dir)
|
|
98
|
+
if not downloaded:
|
|
99
|
+
raise RuntimeError(f"yt-dlp ran successfully but no media file found in {output_dir}")
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
"file_path": downloaded,
|
|
103
|
+
"media_type": detect_media_type(downloaded),
|
|
104
|
+
"source_url": url,
|
|
105
|
+
"title": title or Path(downloaded).stem,
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _resolve_local(path: str) -> dict:
|
|
110
|
+
abs_path = str(Path(path).expanduser().resolve())
|
|
111
|
+
if not os.path.isfile(abs_path):
|
|
112
|
+
raise FileNotFoundError(f"File not found: {abs_path}")
|
|
113
|
+
|
|
114
|
+
return {
|
|
115
|
+
"file_path": abs_path,
|
|
116
|
+
"media_type": detect_media_type(abs_path),
|
|
117
|
+
"source_url": None,
|
|
118
|
+
"title": Path(abs_path).stem,
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _find_media_file(directory: str) -> str | None:
|
|
123
|
+
"""Find the first supported media file in a directory."""
|
|
124
|
+
for f in sorted(os.listdir(directory)):
|
|
125
|
+
if Path(f).suffix.lower() in SUPPORTED_EXTS:
|
|
126
|
+
return os.path.join(directory, f)
|
|
127
|
+
return None
|