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.
@@ -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/
@@ -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
@@ -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
@@ -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