Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
58ae358
Start scraping assets
roboloop Dec 16, 2024
5ebed3d
The tricky logic to determine the beggining, the ending, the first spin
roboloop Dec 17, 2024
c17099f
Another automation to extract lengths of assets
roboloop Dec 18, 2024
7c28fff
Extracting length frames for future tests
roboloop Dec 19, 2024
4e996ba
Added some linters and prettiers
roboloop Dec 20, 2024
c198eec
Structuring the assets
roboloop Dec 20, 2024
add66ea
Setting up the logger with color support
roboloop Dec 20, 2024
56ca560
Correctly set up of pytesseract. Implemented binary search for the fi…
roboloop Dec 21, 2024
d03bfac
Found an edge case with a non-elliptical circle, for now just send on…
roboloop Dec 21, 2024
3ad260a
Length parsing, first implementation
roboloop Dec 21, 2024
209d742
Ask the user for the length if parsing fails
roboloop Dec 21, 2024
f7e18ee
Selecting heuristics for length determination. Discarding false posit…
roboloop Dec 21, 2024
d3a7ef5
Debugging enhancements: skipping the first few seconds of the video a…
roboloop Dec 22, 2024
1993e95
Improvements in spin detection: now it avoids false positive outcomes
roboloop Dec 23, 2024
2999ecd
First implementation of angle detection
roboloop Dec 24, 2024
29770d6
Found edge case when two serial frames are the same. Dirty fix
roboloop Dec 24, 2024
d1f0ea5
Printing the lag time between the current stream and the handling frame
roboloop Dec 24, 2024
8686a4b
Collecting the meta of all assets
roboloop Dec 24, 2024
fcc05fe
Attempting to predict the angle
roboloop Dec 27, 2024
f473283
Research, part 1
roboloop Dec 27, 2024
727d1d0
Ongoing research, collecting data, creating graphs/tables, part 2
roboloop Dec 28, 2024
48d24e0
Tests on length detection
roboloop Dec 28, 2024
3ec5f39
More tests on length detection
roboloop Dec 28, 2024
2c4eb7f
Some performance optimizations, speculating on the calculated angle b…
roboloop Dec 28, 2024
10ec815
Ongoing research into incorrect angle calculation. part 3.
roboloop Dec 29, 2024
1947f12
Research is done. The problem is GSAP. part 4
roboloop Dec 31, 2024
cf99390
Now, the Bezier implementation is based on the GSAP (JS) implementation
roboloop Jan 1, 2025
75943c5
Draft for voting system for seleceting the spin winner
roboloop Jan 2, 2025
216dc61
Extracting the sections
roboloop Jan 2, 2025
77ac21c
Visualization of the steps in sector parsing
roboloop Jan 3, 2025
d9fd324
Colorization for easier debugging
roboloop Jan 3, 2025
3324ea5
Sectors colorization, much better
roboloop Jan 3, 2025
b5f69c2
Adding tests for the extract sections
roboloop Jan 3, 2025
6fbab49
Consolidating all thelogic together, it should work
roboloop Jan 3, 2025
07b2f17
Forgotten measures
roboloop Jan 3, 2025
e3baa14
Detecting the range length and throwing an exception in that case for…
roboloop Jan 6, 2025
868ef25
Detecting the length based on the spin wheel (tricky and hacky)
roboloop Jan 7, 2025
8779917
Shell commands as a shorcut
roboloop Jan 7, 2025
31ff813
Obfuscation command
roboloop Jan 9, 2025
07d23fc
Preview-gif
roboloop Jan 13, 2025
330e463
README
roboloop Jan 13, 2025
6f92b82
Simple ci/cd
roboloop Jan 13, 2025
56fed65
Linter
roboloop Jan 13, 2025
cb78dc3
Fixes to pass all tests
roboloop Jan 13, 2025
c03cbbe
CI/CD
roboloop Jan 13, 2025
4862d7d
First version of wheel detection
roboloop Dec 20, 2024
8998c9c
Using the package manager correctly
roboloop Dec 20, 2024
03fa118
Fixes after Copilot work
roboloop Mar 16, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
*.ts filter=lfs diff=lfs merge=lfs -text
*.mp4 filter=lfs diff=lfs merge=lfs -text
*.m4v filter=lfs diff=lfs merge=lfs -text
*.png filter=lfs diff=lfs merge=lfs -text
*.jpg filter=lfs diff=lfs merge=lfs -text
*.jpeg filter=lfs diff=lfs merge=lfs -text
39 changes: 39 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
name: CI Workflow

on:
push:
branches:
- main
pull_request:

jobs:
test:
runs-on: ubuntu-latest

steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.12'

- name: Install dependencies
run: |
python -m pip install --upgrade pip setuptools
sudo apt-get update && sudo apt-get install -y python3-opencv tesseract-ocr libtesseract-dev libleptonica-dev
pip install -r requirements.txt

- name: Verify installation
run: |
python -c "import cv2; import pytesseract; print(cv2.__version__); print(pytesseract.get_tesseract_version())"

- name: Run Black
run: |
pip install black
black --check .

# - name: Run tests
# run: |
# python -m unittest discover -s tests -t .
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
__pycache__/
*.py[cod]
*$py.class

*.venv

assets/
68 changes: 68 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# Winner Finder

Predict the winner of a wheel spin during live streams (from [pointauc.com](https://pointauc.com/)) before it's determined.

## Description

Want to predict the result of a wheel spin before the winner is announced? Use this tool! Here's a preview of how it works:

![preview.gif](./preview.gif)

## Installation

1. Install the project:

```shell
# clone the repository
git clone

# create a virtual environment
python -m venv myenv

# activate the virtual environment
source ./.venv/bin/activate

# install dependencies
pip install -r requirements.txt
```

2. Install [tesseract](https://tesseract-ocr.github.io/tessdoc/Installation.html), which is required for text recognition in images. Make sure to install the necessary language packs from the [tessdata](https://github.com/tesseract-ocr/tessdata) repository.

3. Install [streamlink](https://github.com/streamlink/streamlink), which allows you to grab a video stream from any platforms (Twitch, YouTube, Kick, etc...).

> ℹ️ [yt_dlp](https://github.com/yt-dlp/yt-dlp) can be used as alternative.

## Run

Run the program before the wheel spin starts:


```shell
streamlink --twitch-low-latency --stdout <channel link> best | python main.py winner

# or you can use a shorthand
./utils run <channel link>
```

## Lint

Run the following commands to lint your code:

```shell
black .
```

## Test

Run the test suite with:

```shell
python -m unittest discover -s tests -t .
```

## TODO list:

- Properly handle ellipsis cases (the wheel isn't always a perfect circle)
- Choose a more flexible solution for the config
- Fix GitHub Actions
- Improve linter setup
41 changes: 41 additions & 0 deletions config/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
from .config import (
ANGLE_WINDOW_LEN,
ASK_LENGTH,
CALCULATION_STEP,
EXCLUDE_SECONDS_RANGE,
FRAMES_STEP_FOR_LENGTH_DETECTION,
LOOK_BEHIND_FRAMES_FOR_LENGTH_DETECTION,
MAX_MEAN_ANGLE_DELTA,
MIN_SKIP_OF_WHEEL_SPIN,
MIN_SKIP_SEC,
NASTY_OPTIMIZATION,
READ_STEP,
SPIN_BUFFER_SIZE,
SPIN_BUFFER_SIZE_FOR_LENGTH_DETECTION,
SPIN_DETECT_LENGTH,
TESSERACT_LANG,
VISUALIZATION_ENABLED,
)
from .logger_config import add_global_event_time, setup_logger, update_global_event_time

__all__ = [
"setup_logger",
"update_global_event_time",
"add_global_event_time",
"READ_STEP",
"ANGLE_WINDOW_LEN",
"CALCULATION_STEP",
"SPIN_BUFFER_SIZE",
"MIN_SKIP_OF_WHEEL_SPIN",
"MIN_SKIP_SEC",
"MAX_MEAN_ANGLE_DELTA",
"NASTY_OPTIMIZATION",
"SPIN_DETECT_LENGTH",
"SPIN_BUFFER_SIZE_FOR_LENGTH_DETECTION",
"FRAMES_STEP_FOR_LENGTH_DETECTION",
"LOOK_BEHIND_FRAMES_FOR_LENGTH_DETECTION",
"EXCLUDE_SECONDS_RANGE",
"ASK_LENGTH",
"TESSERACT_LANG",
"VISUALIZATION_ENABLED",
]
21 changes: 21 additions & 0 deletions config/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
READ_STEP = 2
ANGLE_WINDOW_LEN = 30
CALCULATION_STEP = ANGLE_WINDOW_LEN * 3
SPIN_BUFFER_SIZE = 3
# The close to accurate is 1/4. The most accurate 1/3.
MIN_SKIP_OF_WHEEL_SPIN = 1 / 3
MIN_SKIP_SEC = 10
MAX_MEAN_ANGLE_DELTA = 10.0
TESSERACT_LANG = "rus+eng" # "eng"

NASTY_OPTIMIZATION = True

SPIN_DETECT_LENGTH = True
SPIN_BUFFER_SIZE_FOR_LENGTH_DETECTION = 10
FRAMES_STEP_FOR_LENGTH_DETECTION = 10
LOOK_BEHIND_FRAMES_FOR_LENGTH_DETECTION = 5
EXCLUDE_SECONDS_RANGE = (30, 180)
ASK_LENGTH = True


VISUALIZATION_ENABLED = False
66 changes: 66 additions & 0 deletions config/logger_config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import logging
import time

import colorlog

global_event_time = time.time()


class DynamicAdapter(logging.LoggerAdapter):
def process(self, msg, kwargs):
extra = kwargs.get("extra", {})
extra_str = " ".join([f"{key}={value}" for key, value in extra.items()])
msg = f"{msg} {extra_str}" if extra_str else msg

return msg, kwargs


class TimeDeltaFilter(logging.Filter):
def filter(self, record):
# Calculate the time difference in seconds from the global event
current_time = time.time()
delta_time = current_time - global_event_time
record.time_since_event = delta_time
return True


def setup_logger(level=logging.DEBUG) -> DynamicAdapter:
logger = logging.getLogger("logger")
if logger.hasHandlers():
return DynamicAdapter(logger, {})

logger.setLevel(level)

formatter = colorlog.ColoredFormatter(
"%(asctime)s.%(msecs)03d (%(time_since_event).2fs ago) - %(log_color)s%(levelname)-8s%(reset)s %(white)s%(message)s",
datefmt="%Y-%m-%d %H:%M:%S",
reset=True,
log_colors={
"DEBUG": "cyan",
"INFO": "green",
"WARNING": "yellow",
"ERROR": "red",
"CRITICAL": "red,bg_white",
},
secondary_log_colors={},
style="%",
)

console_handler = logging.StreamHandler()
console_handler.setLevel(level)
console_handler.setFormatter(formatter)

logger.addHandler(console_handler)
logger.addFilter(TimeDeltaFilter())

return DynamicAdapter(logger, {})


def update_global_event_time() -> None:
global global_event_time
global_event_time = time.time()


def add_global_event_time(sec: int) -> None:
global global_event_time
global_event_time += sec
Loading