def __init__(self, key, db_path=None, slider_dir=None, loader=None, \ cache=True): self.log = logging.getLogger(__name__) # allow for people to pass their own loader implementation/subclass. # Mostly exposed for circleguard (the gui). if loader: self.loader = loader else: self.loader = Loader(key, db_path, write_to_cache=cache) if slider_dir: self.library = Library(slider_dir) else: # If slider dir wasn't passed, use a temporary library which will # effectively cache beatmaps for just this cg instance. # Have to keep a reference to this dir or the folder gets deleted. self.slider_dir = TemporaryDirectory() self.library = Library(self.slider_dir.name) # clean up our library (which resides in a temporary dir) or else # garbage collection of this cg object (and subsequently the # temp dir and library) will cause an error to be thrown. This # happens because the temp dir's finalizer is called first, which # tries to remove the directory, but it can't because the library's # sql connection to the db file in that dir is still alive, and a # PermissionError is thrown. We need to close the library before # the temp dir is finalized. # Errors that happen during garbage collection are ignored I # believe, so this only fixes the error message appearing (which is # still a good thing to do) rather than actually fixing any programs # that broke because of this. self._finalizer = weakref.finalize(self, self._cleanup, self.library)
def __init__(self, key, db_path=None, slider_dir=None, loader=None, \ cache=True): self.cache = cache self.cacher = None if db_path is not None: # resolve relative paths db_path = Path(db_path).absolute() # they can set cache to False later with # :func:`~.circleguard.set_options` if they want; assume caching is # desired if db path is passed self.cacher = Cacher(self.cache, db_path) self.log = logging.getLogger(__name__) # allow for people to pass their own loader implementation/subclass LoaderClass = Loader if loader is None else loader self.loader = LoaderClass(key, self.cacher) if slider_dir is None: # have to keep a reference to it or the folder gets deleted and # can't be walked by Library self.slider_dir = TemporaryDirectory() self.library = None else: self.library = Library(slider_dir)
def __init__(self): QFrame.__init__(self) SingleLinkableSetting.__init__(self, "api_key") self.library = Library(get_setting("cache_dir")) self.loadables_combobox = QComboBox(self) self.loadables_combobox.setInsertPolicy(QComboBox.NoInsert) for loadable in MainTab.LOADABLES_COMBOBOX_REGISTRY: self.loadables_combobox.addItem(loadable, loadable) self.loadables_combobox.activated.connect(self.add_loadable) self.checks_combobox = QComboBox(self) self.checks_combobox.setInsertPolicy(QComboBox.NoInsert) for check in MainTab.CHECKS_COMBOBOX_REGISTRY: self.checks_combobox.addItem(check, check) self.checks_combobox.activated.connect(self.add_check) self.loadables_scrollarea = QScrollArea(self) self.loadables_scrollarea.setWidget(ScrollableLoadablesWidget()) self.loadables_scrollarea.setWidgetResizable(True) self.checks_scrollarea = QScrollArea(self) self.checks_scrollarea.setWidget(ScrollableChecksWidget()) self.checks_scrollarea.setWidgetResizable(True) self.loadables = [] # for deleting later self.checks = [] # for deleting later self.print_results_signal.connect(self.print_results) self.write_to_terminal_signal.connect(self.write) self.q = Queue() self.cg_q = Queue() self.helper_thread_running = False self.runs = [] # Run objects for canceling runs self.run_id = 0 self.visualizer = None terminal = QTextEdit(self) terminal.setFocusPolicy(Qt.ClickFocus) terminal.setReadOnly(True) terminal.ensureCursorVisible() self.terminal = terminal self.run_button = QPushButton() self.run_button.setText("Run") self.run_button.clicked.connect(self.add_circleguard_run) # disable button if no api_key is stored self.on_setting_changed("api_key", get_setting("api_key")) layout = QGridLayout() layout.addWidget(self.loadables_combobox, 0, 0, 1, 4) layout.addWidget(self.checks_combobox, 0, 8, 1, 4) layout.addWidget(self.loadables_scrollarea, 1, 0, 4, 8) layout.addWidget(self.checks_scrollarea, 1, 8, 4, 8) layout.addWidget(self.terminal, 5, 0, 2, 16) layout.addWidget(self.run_button, 7, 0, 1, 16) self.setLayout(layout)
def run_job(user, replay_cache_dir, model_cache_dir, age, library, api_key): import pathlib from lain import ErrorModel from lain.train import load_replay_directory import pandas as pd from slider import Client, Library from combine.utils import model_path osu_client = Client(Library(library), api_key) replays = load_replay_directory( pathlib.Path(replay_cache_dir) / user, client=osu_client, age=pd.Timedelta(age) if age is not None else None, save=True, verbose=True, ) model = ErrorModel() model.fit(replays) user_models = model_path(model_cache_dir, user) user_models.mkdir(parents=True, exist_ok=True) model.save_path(user_models)
import scipy from badgeWidget import VisualizerWindow from PyQt5.QtWidgets import QApplication USER = "******" MAP = "2097898" OSU_API_KEY = "" USE_REPLAY = False # if the file should be used instead of downloading the replay REPLAY_PATH = "./replay/nonexistingfile.osr" CACHE_DIR = "./cache/" _api = ossapi(OSU_API_KEY) _cg = Circleguard(OSU_API_KEY) _loader = _cg.loader if not os.path.exists(CACHE_DIR): os.mkdir(CACHE_DIR) _library = Library.create_db(CACHE_DIR) def _get_score_info(): print("@retrieving score info") return _api.get_scores({"b":MAP, "u":USER})[0] def _get_replay(score_info): if USE_REPLAY: print("@USE_REPLAY set, using REPLAY_PATH") replay = ReplayPath(REPLAY_PATH) else: print("@USE_REPLAY not set, trying to download replay") if score_info["replay_available"] != "0": print("@Downloading Replay") replay = ReplayMap(user_id=USER,map_id=MAP)
def run(self, loadables, detect, loadables2=None, max_angle=DEFAULT_ANGLE, \ min_distance=DEFAULT_DISTANCE, num_chunks=DEFAULT_CHUNKS) \ -> Iterable[Result]: """ Investigates loadables for cheats. Parameters ---------- loadables: list[:class:`~.Loadable`] The loadables to investigate. detect: :class:`~.Detect` What cheats to investigate for. loadables2: list[:class:`~.Loadable`] For :data:`~Detect.STEAL`, compare each loadable in ``loadables`` against each loadable in ``loadables2`` for replay stealing, instead of to other loadables in ``loadables``. max_angle: float For :data:`Detect.CORRECTION`, consider only points (a,b,c) where ``∠abc < max_angle``. min_distance: float For :data:`Detect.CORRECTION`, consider only points (a,b,c) where ``|ab| > min_distance`` and ``|bc| > min_distance``. num_chunks: int For :data:`detect.STEAL_CORR`, how many chunks to split the replay into when comparing. Note that runtime increases linearly with the number of chunks. Yields ------ :class:`~.Result` A result representing an investigation of one or more of the replays in ``loadables``, depending on the ``detect`` passed. Notes ----- :class:`~.Result`\s are yielded one at a time, as circleguard finishes investigating them. This means that you can process results from :meth:`~.run` without waiting for all of the investigations to finish. """ c = Check(loadables, self.cache, loadables2=loadables2) self.log.info("Running circleguard with check %r", c) c.load(self.loader) # comparer investigations if detect & (Detect.STEAL_SIM | Detect.STEAL_CORR): replays1 = c.all_replays1() replays2 = c.all_replays2() comparer = Comparer(replays1, replays2, detect, num_chunks) yield from comparer.compare() # investigator investigations if detect & (Detect.RELAX | Detect.CORRECTION | Detect.TIMEWARP): if detect & Detect.RELAX: if not self.library: # connect to library since it's a temporary one library = Library(self.slider_dir.name) else: library = self.library for replay in c.all_replays(): bm = None # don't download beatmap unless we need it for relax if detect & Detect.RELAX: bm = library.lookup_by_id(replay.map_id, download=True, \ save=True) investigator = Investigator(replay, detect, max_angle, \ min_distance, beatmap=bm) yield from investigator.investigate() if detect & Detect.RELAX: if not self.library: # disconnect from temporary library library.close()
def library(self): if not self._library: from slider import Library self._library = Library(get_setting("cache_dir")) return self._library
def __init__(self, beatmap_info, replays, events, library, speeds, \ start_speed, paint_info, statistic_functions, snaps_args): super().__init__() self.speeds = speeds self.replays = replays self.library = library self.snaps_args = snaps_args self.current_replay_info = None # maps `circleguard.Replay` to `circlevis.ReplayInfo`, as its creation # is relatively expensive and users might open and close the same info # panel multiple times self.replay_info_cache = {} # we calculate some statistics in the background so users aren't hit # with multi-second wait times when accessing replay info. Initialize # with `None` so if the replay info *is* accessed before we calculate # everything, no harm - `ReplayInfo` will calculate it instead. self.replay_statistics_precalculated = {} for replay in replays: self.replay_statistics_precalculated[replay] = (None, None, None, None) # only precalculate statistics if we're visualizing 5 or fewer replays. # Otherwise, the thread is too overworked and lags the main draw thread # significantly until all statistics are precalculated. # TODO This may be resolved properly by using a QThread with a low # priority instead, so as not to starve the draw thread. We should be # using QThreads instead of python threads regardless. if len(replays) <= 5: # and here's the thread which will actually start those calculations cg_statistics_worked = Thread(target=self.calculate_cg_statistics) # allow users to quit before we're done calculating cg_statistics_worked.daemon = True cg_statistics_worked.start() # create our own library in a temp dir if one wasn't passed if not self.library: # keep a reference so it doesn't get deleted self.temp_dir = TemporaryDirectory() self.library = Library(self.temp_dir.name) self.beatmap = None if beatmap_info.path: self.beatmap = Beatmap.from_path(beatmap_info.path) elif beatmap_info.map_id: # TODO move temporary directory creation to slider probably, since # this logic is now duplicated here and in circlecore self.beatmap = self.library.lookup_by_id(beatmap_info.map_id, download=True, save=True) dt_enabled = any(Mod.DT in replay.mods for replay in replays) ht_enabled = any(Mod.HT in replay.mods for replay in replays) if dt_enabled: start_speed = 1.5 if ht_enabled: start_speed = 0.75 self.renderer = Renderer(self.beatmap, replays, events, start_speed, paint_info, statistic_functions) self.renderer.update_time_signal.connect(self.update_slider) # if the renderer wants to pause itself (eg when the playback hits the # end of the replay), we kick it back to us (the `Interface`) so we can # also update the pause button's state. self.renderer.pause_signal.connect(self.toggle_pause) # we want to give `VisualizerControls` the union of all the replay's # mods mods = Mod.NM for replay in replays: mods += replay.mods self.controls = VisualizerControls(start_speed, mods, replays) self.controls.pause_button.clicked.connect(self.toggle_pause) self.controls.play_reverse_button.clicked.connect(self.play_reverse) self.controls.play_normal_button.clicked.connect(self.play_normal) self.controls.next_frame_button.clicked.connect( lambda: self.change_frame(reverse=False)) self.controls.previous_frame_button.clicked.connect( lambda: self.change_frame(reverse=True)) self.controls.speed_up_button.clicked.connect(self.increase_speed) self.controls.speed_down_button.clicked.connect(self.lower_speed) self.controls.copy_to_clipboard_button.clicked.connect( self.copy_to_clipboard) self.controls.time_slider.sliderMoved.connect(self.renderer.seek_to) self.controls.time_slider.setRange(self.renderer.playback_start, self.renderer.playback_end) self.controls.raw_view_changed.connect(self.renderer.raw_view_changed) self.controls.only_color_keydowns_changed.connect( self.renderer.only_color_keydowns_changed) self.controls.hitobjects_changed.connect( self.renderer.hitobjects_changed) self.controls.approach_circles_changed.connect( self.renderer.approach_circles_changed) self.controls.num_frames_changed.connect( self.renderer.num_frames_changed) self.controls.draw_hit_error_bar_changed.connect( self.renderer.draw_hit_error_bar_changed) self.controls.circle_size_mod_changed.connect( self.renderer.circle_size_mod_changed) self.controls.show_info_for_replay.connect(self.show_info_panel) self.splitter = QSplitter() # splitter lays widgets horizontally by default, so combine renderer and # controls into one single widget vertically self.splitter.addWidget( Combined([self.renderer, self.controls], Qt.Vertical)) layout = QGridLayout() layout.addWidget(self.splitter, 1, 0, 1, 1) layout.setContentsMargins(0, 0, 0, 0) self.setLayout(layout)
class Interface(QWidget): def __init__(self, beatmap_info, replays, events, library, speeds, \ start_speed, paint_info, statistic_functions, snaps_args): super().__init__() self.speeds = speeds self.replays = replays self.library = library self.snaps_args = snaps_args self.current_replay_info = None # maps `circleguard.Replay` to `circlevis.ReplayInfo`, as its creation # is relatively expensive and users might open and close the same info # panel multiple times self.replay_info_cache = {} # we calculate some statistics in the background so users aren't hit # with multi-second wait times when accessing replay info. Initialize # with `None` so if the replay info *is* accessed before we calculate # everything, no harm - `ReplayInfo` will calculate it instead. self.replay_statistics_precalculated = {} for replay in replays: self.replay_statistics_precalculated[replay] = (None, None, None, None) # only precalculate statistics if we're visualizing 5 or fewer replays. # Otherwise, the thread is too overworked and lags the main draw thread # significantly until all statistics are precalculated. # TODO This may be resolved properly by using a QThread with a low # priority instead, so as not to starve the draw thread. We should be # using QThreads instead of python threads regardless. if len(replays) <= 5: # and here's the thread which will actually start those calculations cg_statistics_worked = Thread(target=self.calculate_cg_statistics) # allow users to quit before we're done calculating cg_statistics_worked.daemon = True cg_statistics_worked.start() # create our own library in a temp dir if one wasn't passed if not self.library: # keep a reference so it doesn't get deleted self.temp_dir = TemporaryDirectory() self.library = Library(self.temp_dir.name) self.beatmap = None if beatmap_info.path: self.beatmap = Beatmap.from_path(beatmap_info.path) elif beatmap_info.map_id: # TODO move temporary directory creation to slider probably, since # this logic is now duplicated here and in circlecore self.beatmap = self.library.lookup_by_id(beatmap_info.map_id, download=True, save=True) dt_enabled = any(Mod.DT in replay.mods for replay in replays) ht_enabled = any(Mod.HT in replay.mods for replay in replays) if dt_enabled: start_speed = 1.5 if ht_enabled: start_speed = 0.75 self.renderer = Renderer(self.beatmap, replays, events, start_speed, paint_info, statistic_functions) self.renderer.update_time_signal.connect(self.update_slider) # if the renderer wants to pause itself (eg when the playback hits the # end of the replay), we kick it back to us (the `Interface`) so we can # also update the pause button's state. self.renderer.pause_signal.connect(self.toggle_pause) # we want to give `VisualizerControls` the union of all the replay's # mods mods = Mod.NM for replay in replays: mods += replay.mods self.controls = VisualizerControls(start_speed, mods, replays) self.controls.pause_button.clicked.connect(self.toggle_pause) self.controls.play_reverse_button.clicked.connect(self.play_reverse) self.controls.play_normal_button.clicked.connect(self.play_normal) self.controls.next_frame_button.clicked.connect( lambda: self.change_frame(reverse=False)) self.controls.previous_frame_button.clicked.connect( lambda: self.change_frame(reverse=True)) self.controls.speed_up_button.clicked.connect(self.increase_speed) self.controls.speed_down_button.clicked.connect(self.lower_speed) self.controls.copy_to_clipboard_button.clicked.connect( self.copy_to_clipboard) self.controls.time_slider.sliderMoved.connect(self.renderer.seek_to) self.controls.time_slider.setRange(self.renderer.playback_start, self.renderer.playback_end) self.controls.raw_view_changed.connect(self.renderer.raw_view_changed) self.controls.only_color_keydowns_changed.connect( self.renderer.only_color_keydowns_changed) self.controls.hitobjects_changed.connect( self.renderer.hitobjects_changed) self.controls.approach_circles_changed.connect( self.renderer.approach_circles_changed) self.controls.num_frames_changed.connect( self.renderer.num_frames_changed) self.controls.draw_hit_error_bar_changed.connect( self.renderer.draw_hit_error_bar_changed) self.controls.circle_size_mod_changed.connect( self.renderer.circle_size_mod_changed) self.controls.show_info_for_replay.connect(self.show_info_panel) self.splitter = QSplitter() # splitter lays widgets horizontally by default, so combine renderer and # controls into one single widget vertically self.splitter.addWidget( Combined([self.renderer, self.controls], Qt.Vertical)) layout = QGridLayout() layout.addWidget(self.splitter, 1, 0, 1, 1) layout.setContentsMargins(0, 0, 0, 0) self.setLayout(layout) def play_normal(self): self.unpause() self.renderer.play_direction = 1 self.update_speed(abs(self.renderer.clock.current_speed)) def update_slider(self, value): self.controls.time_slider.setValue(value) def change_by(self, delta): self.pause() self.renderer.seek_to(self.renderer.clock.time_counter + delta) def play_reverse(self): self.unpause() self.renderer.play_direction = -1 self.update_speed(abs(self.renderer.clock.current_speed)) def update_speed(self, speed): self.renderer.clock.change_speed(speed * self.renderer.play_direction) def change_frame(self, reverse): self.pause() self.renderer.search_nearest_frame(reverse=reverse) def toggle_pause(self): if self.renderer.paused: self.unpause() else: self.pause() def pause(self): self.controls.set_paused_state(True) self.renderer.pause() def unpause(self): self.controls.set_paused_state(False) self.renderer.resume() def lower_speed(self): index = self.speeds.index(abs(self.renderer.clock.current_speed)) if index == 0: return speed = self.speeds[index - 1] self.controls.speed_label.setText(str(speed) + "x") self.update_speed(speed) def increase_speed(self): index = self.speeds.index(abs(self.renderer.clock.current_speed)) if index == len(self.speeds) - 1: return speed = self.speeds[index + 1] self.controls.speed_label.setText(str(speed) + "x") self.update_speed(speed) def copy_to_clipboard(self): timestamp = int(self.renderer.clock.get_time()) clipboard = QApplication.clipboard() # TODO accomodate arbitrary numbers of replays (including 0 replays) r1 = self.replays[0] if len(self.replays) == 2: r2 = self.replays[1] user_str = f"u={r1.user_id}&m1={r1.mods.short_name()}&u2={r2.user_id}&m2={r2.mods.short_name()}" else: user_str = f"u={r1.user_id}&m1={r1.mods.short_name()}" clipboard.setText( f"circleguard://m={r1.map_id}&{user_str}&t={timestamp}") def show_info_panel(self, replay): """ Shows an info panel containing stats about the replay to the left of the renderer. The visualizer window will expand to accomodate for this extra space. """ if replay in self.replay_info_cache: replay_info = self.replay_info_cache[replay] replay_info.show() else: ur, frametime, snaps, judgments = self.replay_statistics_precalculated[ replay] replay_info = ReplayInfo(replay, self.library.path, ur, frametime, snaps, judgments, self.snaps_args) replay_info.seek_to.connect(self.seek_to) # don't show two of the same info panels at once if self.current_replay_info is not None: # if they're the same, don't change anything if replay_info == self.current_replay_info: return # Otherwise, close the current one and show the new one. # simulate a "close" button press self.current_replay_info.close_button_clicked.emit() def remove_replay_info(): replay_info.hide() self.current_replay_info = None replay_info.close_button_clicked.connect(remove_replay_info) self.splitter.insertWidget(0, replay_info) self.current_replay_info = replay_info self.replay_info_cache[replay] = replay_info def seek_to(self, time): self.pause() self.renderer.seek_to(time) def calculate_cg_statistics(self): cg = KeylessCircleguard() for replay in self.replays: ur = None judgments = None if cg.map_available(replay): ur = cg.ur(replay) judgments = cg.judgments(replay) frametime = cg.frametime(replay) snaps = cg.snaps(replay, **self.snaps_args) self.replay_statistics_precalculated[replay] = (ur, frametime, snaps, judgments)