Merge branch 'main' of https://github.com/xlang-ai/DesktopEnv
19
.vscode/launch.json
vendored
@@ -1,19 +0,0 @@
|
||||
{
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Python Debugger: Current File with Arguments",
|
||||
"type": "debugpy",
|
||||
"request": "launch",
|
||||
"program": "${file}",
|
||||
"console": "integratedTerminal",
|
||||
"args": [
|
||||
"--path_to_vm", "/Users/lxc/Virtual Machines.localized/DesktopEnv-Ubuntu 64-bit Arm.vmwarevm/DesktopEnv-Ubuntu 64-bit Arm.vmx"
|
||||
// "--example_time_limit", "60"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -178,7 +178,7 @@
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "{}"
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
@@ -186,7 +186,7 @@
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright contributors
|
||||
Copyright 2024 XLANG NLP Lab
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
151
README.md
@@ -1,92 +1,123 @@
|
||||
# OSWorld: Open-Ended Tasks in Real Computer Environments
|
||||
# OSWorld: Benchmarking Multimodal Agents for Open-Ended Tasks in Real Computer Environments
|
||||
|
||||
<p align="center">
|
||||
<img src="desktop_env/assets/icon.jpg" alt="Logo" width="80px">
|
||||
<br>
|
||||
<b>SLOGAN</b>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="">Website</a> •
|
||||
<a href="https://os-world.github.io/">Website</a> •
|
||||
<a href="">Paper</a>
|
||||
</p>
|
||||
|
||||
![Overview]()
|
||||
|
||||
## Updates
|
||||
- 2024-03-01: We released our [paper](), [environment code](), [dataset](), and [project page](). Check it out!
|
||||
- 2024-04-04: We released our [paper](), [environment and benchmark](https://github.com/xlang-ai/OSWorld), and [project page](https://os-world.github.io/). Check it out!
|
||||
|
||||
## Install
|
||||
1. Install VMWare and configure `vmrun` command:
|
||||
Please refer to [guidance](https://docs.google.com/document/d/1KBdeZwmZs2Vi_Wsnngb3Wf1-RiwMMpXTftwMqP2Ztak/edit#heading=h.uh0x0tkl7fuw)
|
||||
## Installation
|
||||
### On Your Desktop or Server (Non-Virtualized Platform)
|
||||
Suppose you are operating on a system that has not been virtualized, meaning you are not utilizing a virtualized environment like AWS, Azure, or k8s. If this is the case, proceed with the instructions below. However, if you are on a virtualized platform, please refer to the [virtualized platform](https://github.com/xlang-ai/OSWorld?tab=readme-ov-file#virtualized-platform) section.
|
||||
|
||||
2. Install the environment package, download the examples and the virtual machine image.
|
||||
For x86_64 Linux or Windows, you can install the environment package and download the examples and the virtual machine image by running the following commands:
|
||||
1. First, clone this repository and `cd` into it. Then, install the dependencies listed in `requirements.txt`. It is recommended that you use the latest version of Conda to manage the environment, but you can also choose to manually install the dependencies. Please ensure that the version of Python is >= 3.9.
|
||||
```bash
|
||||
git clone https://github.com/xlang-ai/DesktopEnv
|
||||
cd DesktopEnv
|
||||
# Clone the OSWorld repository
|
||||
git clone https://github.com/xlang-ai/OSWorld
|
||||
|
||||
# Change directory into the cloned repository
|
||||
cd OSWorld
|
||||
|
||||
# Optional: Create a Conda environment for OSWorld
|
||||
# conda create -n osworld python=3.9
|
||||
# conda activate osworld
|
||||
|
||||
# Install required dependencies
|
||||
pip install -r requirements.txt
|
||||
gdown https://drive.google.com/drive/folders/1HX5gcf7UeyR-2UmiA15Q9U-
|
||||
Wr6E6Gio8 -O Ubuntu --folder
|
||||
```
|
||||
|
||||
2. Install [VMware Workstation Pro](https://www.vmware.com/products/workstation-pro/workstation-pro-evaluation.html) (for systems with Apple Chips, you should install [VMware Fusion](https://www.vmware.com/go/getfusion)) and configure the `vmrun` command. Verify the successful installation by running the following:
|
||||
```bash
|
||||
vmrun -T ws list
|
||||
```
|
||||
If the installation along with the environment variable set is successful, you will see the message showing the current running virtual machines.
|
||||
|
||||
3. Obtain the virtual machine image. If you are using Linux or Windows with an x86_64 CPU, install the environment package and download the examples and the virtual machine image by executing the following commands:
|
||||
Remove the `nogui` parameter if you wish to view the activities within the virtual machine.
|
||||
```bash
|
||||
gdown https://drive.google.com/drive/folders/1HX5gcf7UeyR-2UmiA15Q9U-Wr6E6Gio8 -O Ubuntu --folder
|
||||
vmrun -T ws start "Ubuntu/Ubuntu.vmx" nogui
|
||||
vmrun -T ws snapshot "Ubuntu/Ubuntu.vmx" "init_state"
|
||||
```
|
||||
|
||||
For macOS with Apple chips, you should install the specially prepared virtual machine image by executing the following commands:
|
||||
```bash
|
||||
gdown https://drive.google.com/drive/folders/1wT0vwpuEFTIPik9Tjn4DWoZ2oHCD7tM0 -O Ubuntu --folder
|
||||
vmrun -T fusion start "Ubuntu/DesktopEnv-Ubuntu 64-bit Arm.vmx"
|
||||
vmrun -T fusion snapshot "Ubuntu/DesktopEnv-Ubuntu 64-bit Arm.vmx" "init_state"
|
||||
```
|
||||
|
||||
### On AWS or Azure (Virtualized platform)
|
||||
We are working on supporting it 👷. Please hold tight!
|
||||
|
||||
## Quick Start
|
||||
Run the following minimal example to interact with the environment:
|
||||
```python
|
||||
import json
|
||||
from desktop_env.envs.desktop_env import DesktopEnv
|
||||
|
||||
with open("evaluation_examples/examples/gimp/f723c744-e62c-4ae6-98d1-750d3cd7d79d.json", "r", encoding="utf-8") as f:
|
||||
example = json.load(f)
|
||||
example = {
|
||||
"id": "94d95f96-9699-4208-98ba-3c3119edf9c2",
|
||||
"instruction": "I want to install Spotify on my current system. Could you please help me?",
|
||||
"config": [
|
||||
{
|
||||
"type": "execute",
|
||||
"parameters": {
|
||||
"command": [
|
||||
"python",
|
||||
"-c",
|
||||
"import pyautogui; import time; pyautogui.click(960, 540); time.sleep(0.5);"
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"evaluator": {
|
||||
"func": "check_include_exclude",
|
||||
"result": {
|
||||
"type": "vm_command_line",
|
||||
"command": "which spotify"
|
||||
},
|
||||
"expected": {
|
||||
"type": "rule",
|
||||
"rules": {
|
||||
"include": ["spotify"],
|
||||
"exclude": ["not found"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
env = DesktopEnv(
|
||||
path_to_vm=r"path_to_vm",
|
||||
action_space="computer_13",
|
||||
task_config=example
|
||||
path_to_vm=r"Ubuntu/DesktopEnv-Ubuntu 64-bit Arm.vmx",
|
||||
action_space="pyautogui"
|
||||
)
|
||||
observation = env.reset()
|
||||
|
||||
observation, reward, done, info = env.step({"action_type": "CLICK", "parameters": {"button": "right", "num_clicks": 1}})
|
||||
obs = env.reset(task_config=example)
|
||||
obs, reward, done, info = env.step("pyautogui.rightClick()")
|
||||
```
|
||||
You will see all the logs of the system running normally, including the successful creation of the environment, completion of setup, and successful execution of actions. In the end, you will observe a successful right-click on the screen, which means you are ready to go.
|
||||
|
||||
## Experiments
|
||||
### Agent Baselines
|
||||
If you wish to run the baseline agent used in our paper, you can execute the following command as an example under the GPT-4V pure-screenshot setting:
|
||||
```bash
|
||||
python run.py --path_to_vm Ubuntu/Ubuntu.vmx --headless --observation_type screenshot --model gpt-4-vision-preview --result_dir ./results
|
||||
```
|
||||
The results, which include screenshots, actions, and video recordings of the agent's task completion, will be saved in the `./results` directory in this case. You can then run the following command to obtain the result:
|
||||
```bash
|
||||
python show_result.py
|
||||
```
|
||||
|
||||
## Annotation Tool Usage
|
||||
We provide an annotation tool to help you annotate the examples.
|
||||
|
||||
## Agent Usage
|
||||
We provide a simple agent to interact with the environment. You can use it as a starting point to build your own agent.
|
||||
|
||||
## Road map of infra (Proposed)
|
||||
|
||||
- [x] Explore VMWare, and whether it can be connected and control through mouse package
|
||||
- [x] Explore Windows and MacOS, whether it can be installed
|
||||
- MacOS is closed source and cannot be legally installed
|
||||
- Windows is available legally and can be installed
|
||||
- [x] Build gym-like python interface for controlling the VM
|
||||
- [x] Recording of actions (mouse movement, click, keyboard) for humans to annotate, and we can replay it and compress it
|
||||
- [x] Build a simple task, e.g. open a browser, open a website, click on a button, and close the browser
|
||||
- [x] Set up a pipeline and build agents implementation (zero-shot) for the task
|
||||
- [x] Start to design on which tasks inside the DesktopENv to focus on, start to wrap up the environment to be public
|
||||
- [x] Start to annotate the examples for ~~training~~ and testing
|
||||
- [x] Error handling during file passing and file opening, etc.
|
||||
- [x] Add accessibility tree from the OS into the observation space
|
||||
- [x] Add pre-process and post-process action support for benchmarking setup and evaluation
|
||||
- [ ] Multiprocess support, this can enable the reinforcement learning to be more efficient
|
||||
- [ ] Experiment logging and visualization system
|
||||
- [ ] Add more tasks, maybe scale to 300 for v1.0.0, and create a dynamic leaderboard
|
||||
|
||||
## Road map of benchmark, tools and resources (Proposed)
|
||||
- [ ] Improve the annotation tool base on DuckTrack, make it more robust which align on accessibility tree
|
||||
- [ ] Annotate the steps of doing the task
|
||||
- [ ] Build a website for the project
|
||||
- [ ] Crawl all resources we explored from the internet, and make it easy to access
|
||||
- [ ] Set up ways for community to contribute new examples
|
||||
### Evaluation
|
||||
Please start by reading through the [agent interface](https://github.com/xlang-ai/OSWorld/blob/main/mm_agents/README.md) and the [environment interface](https://github.com/xlang-ai/OSWorld/blob/main/desktop_env/README.md).
|
||||
Correctly implement the agent interface and import your customized version in the `run.py` file.
|
||||
Afterward, you can execute a command similar to the one in the previous section to run the benchmark on your agent.
|
||||
|
||||
## Citation
|
||||
If you find this environment useful, please consider citing our work:
|
||||
```
|
||||
@article{DesktopEnv,
|
||||
@article{OSWorld,
|
||||
title={},
|
||||
author={},
|
||||
journal={arXiv preprint arXiv:xxxx.xxxx},
|
||||
|
||||
30
ROADMAP.md
Normal file
@@ -0,0 +1,30 @@
|
||||
# Road Map
|
||||
Here we provide a high-level road map for the project. We will update this road map as we make progress.
|
||||
If you are interested in contributing to the project, please check the [CONTRIBUTING.md](CONTRIBUTING.md) for more details.
|
||||
|
||||
## Road Map for Environment Infrastructure
|
||||
|
||||
- [x] Explore VMWare, and whether it can be connected and control through mouse package
|
||||
- [x] Explore Windows and MacOS, whether it can be installed
|
||||
- MacOS is closed source and cannot be legally installed
|
||||
- Windows is available legally and can be installed
|
||||
- [x] Build gym-like python interface for controlling the VM
|
||||
- [x] Recording of actions (mouse movement, click, keyboard) for humans to annotate, and we can replay it and compress it
|
||||
- [x] Build a simple task, e.g. open a browser, open a website, click on a button, and close the browser
|
||||
- [x] Set up a pipeline and build agents implementation (zero-shot) for the task
|
||||
- [x] Start to design on which tasks inside the DesktopENv to focus on, start to wrap up the environment to be public
|
||||
- [x] Start to annotate the examples for ~~training~~ and testing
|
||||
- [x] Error handling during file passing and file opening, etc.
|
||||
- [x] Add accessibility tree from the OS into the observation space
|
||||
- [x] Add pre-process and post-process action support for benchmarking setup and evaluation
|
||||
- [x] Experiment logging and visualization system
|
||||
- [x] Add more tasks, maybe scale to 300 for v1.0.0, and create a dynamic leaderboard
|
||||
- [x] Multiprocess support, this can enable the reinforcement learning to be more efficient
|
||||
- [ ] Support running on platform that have nested virtualization, e.g. Google Cloud, AWS, etc.
|
||||
|
||||
|
||||
## Road Map of Annotation Tool
|
||||
- [ ] Improve the annotation tool base on DuckTrack, make it more robust which align on accessibility tree
|
||||
- [ ] Annotate the steps of doing the task
|
||||
- [ ] Crawl all resources we explored from the internet, and make it easy to access
|
||||
- [ ] Set up ways for community to contribute new examples
|
||||
172
annotation/.gitignore
vendored
@@ -1,172 +0,0 @@
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
share/python-wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.nox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
*.py,cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
cover/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Django stuff:
|
||||
*.log
|
||||
local_settings.py
|
||||
db.sqlite3
|
||||
db.sqlite3-journal
|
||||
|
||||
# Flask stuff:
|
||||
instance/
|
||||
.webassets-cache
|
||||
|
||||
# Scrapy stuff:
|
||||
.scrapy
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
.pybuilder/
|
||||
target/
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# IPython
|
||||
profile_default/
|
||||
ipython_config.py
|
||||
|
||||
# pyenv
|
||||
# For a library or package, you might want to ignore these files since the code is
|
||||
# intended to run in multiple environments; otherwise, check them in:
|
||||
# .python-version
|
||||
|
||||
# pipenv
|
||||
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||
# install all needed dependencies.
|
||||
#Pipfile.lock
|
||||
|
||||
# poetry
|
||||
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
||||
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||
# commonly ignored for libraries.
|
||||
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
||||
#poetry.lock
|
||||
|
||||
# pdm
|
||||
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
||||
#pdm.lock
|
||||
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
||||
# in version control.
|
||||
# https://pdm.fming.dev/#use-with-ide
|
||||
.pdm.toml
|
||||
|
||||
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
||||
__pypackages__/
|
||||
|
||||
# Celery stuff
|
||||
celerybeat-schedule
|
||||
celerybeat.pid
|
||||
|
||||
# SageMath parsed files
|
||||
*.sage.py
|
||||
|
||||
# Environments
|
||||
.env
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
.spyproject
|
||||
|
||||
# Rope project settings
|
||||
.ropeproject
|
||||
|
||||
# mkdocs documentation
|
||||
/site
|
||||
|
||||
# mypy
|
||||
.mypy_cache/
|
||||
.dmypy.json
|
||||
dmypy.json
|
||||
|
||||
# Pyre type checker
|
||||
.pyre/
|
||||
|
||||
# pytype static type analyzer
|
||||
.pytype/
|
||||
|
||||
# Cython debug symbols
|
||||
cython_debug/
|
||||
|
||||
# PyCharm
|
||||
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
||||
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
||||
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||
#.idea/
|
||||
|
||||
# experiments
|
||||
experiments/**/*.png
|
||||
experiments/**/*.csv
|
||||
experiments/**/*.mp4
|
||||
experiments/**/*.jsonl
|
||||
experiments/**/*.json
|
||||
experiments/**/*.md
|
||||
experiments/**/*.txt
|
||||
|
||||
# macos
|
||||
*DS_Store*
|
||||
@@ -1,21 +0,0 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2023 DuckAI
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -1,50 +0,0 @@
|
||||
# OBS Setup
|
||||
|
||||
These are instructions on setting up OBS (Open Broadcaster Software) to record screen activity for creating the multimodal computer dataset.
|
||||
|
||||
## Installation
|
||||
|
||||
1. Go to the OBS Project website: [https://obsproject.com/](https://obsproject.com/).
|
||||
2. Choose the appropriate installer for your operating system.
|
||||
3.
|
||||

|
||||
|
||||
3. Run the installer from your downloads folder and grant OBS the necessary permissions for installation.
|
||||
|
||||

|
||||
|
||||
4. Keep the default settings and proceed through the installation wizard by clicking "Next" and then "Finish."
|
||||
|
||||

|
||||
|
||||
5. OBS should now be open. If not, search for and open the application.
|
||||
|
||||

|
||||
|
||||
## Enabling OBS WebSocket Server
|
||||
|
||||
1. Click on "Tools" in the Navigation Bar within OBS, and then select "WebSocket Server Settings." A pop-up window will appear.
|
||||
|
||||

|
||||
|
||||
2. Check the box next to "Enable WebSocket server" and uncheck the box next to "Enable Authentication." Click "Apply," then "Ok." You should return to the main OBS page.
|
||||
Make sure the port is set to 4455.
|
||||

|
||||
|
||||
## Adding Display Capture and Recording
|
||||
|
||||
1. Now, back on the home page of OBS, select "Scene." Under "Sources," click the "+" button and then click "Display Capture." (in MacOS this is MacOS Screen Capture)
|
||||
|
||||

|
||||
|
||||
2. Select "Ok."
|
||||
|
||||

|
||||
|
||||
3. Make sure the "Display" is set to your main display, and you should see your screen on the canvas. Select "Ok." _(in MacOS if your screen is black with a red square in the top left try to disable then re-enable OBS Screen Recording permissions, this has worked before)_
|
||||
|
||||

|
||||
|
||||
4. Now you can close OBS and OBS will opened and controlled automatically when you launch the Computer Tracker App. Also, the Computer Tracker app creates a new OBS profile so you don't have to worry about your previous settings being messed up.
|
||||
|
||||

|
||||
@@ -1,98 +0,0 @@
|
||||
# DuckTrack
|
||||
|
||||
This is the repository for the DuckAI DuckTrack app which records all keyboard and mouse input as well as the screen for use in a multimodal computer interaction dataset.
|
||||
|
||||
## Installation & Setup
|
||||
|
||||
### Download Application
|
||||
|
||||
<!-- TODO: add prebuilt applications in github releases -->
|
||||
Download the pre-built application for your system [here](https://github.com/TheDuckAI/DuckTrack/releases/).
|
||||
|
||||
Make sure you have OBS downloaded with the following configuration:
|
||||
1. Have a screen capture source recording your whole main screen.
|
||||
2. Enable desktop audio and mute microphone.
|
||||
3. Make sure the default websocket is enabled.
|
||||
|
||||
More detailed instructions for OBS setup and installation located [here](OBS_SETUP.md).
|
||||
|
||||
If you are on MacOS, make sure to enable to the following Privacy & Security permissions before running the app:
|
||||
|
||||
1. Accessibility (for playing back actions)
|
||||
2. Input Monitoring (for reading keyboard inputs)
|
||||
|
||||
Make sure to accept all other security permission dialogues to ensure that the app works properly.
|
||||
|
||||
### Build from source
|
||||
|
||||
Have Python >=3.11.
|
||||
|
||||
Clone this repo and `cd` into it:
|
||||
```bash
|
||||
$ git clone https://github.com/TheDuckAI/DuckTrack
|
||||
$ cd DuckTrack
|
||||
```
|
||||
|
||||
Install the dependencies for this project:
|
||||
```bash
|
||||
$ pip install -r requirements.txt
|
||||
```
|
||||
|
||||
Build the application:
|
||||
```bash
|
||||
$ python3 build.py
|
||||
```
|
||||
|
||||
The built application should be located in the generated `dist` directory. After this, follow the remaining relevant setup instructions.
|
||||
|
||||
## Running the App
|
||||
|
||||
You can run the app like any other desktop app on your computer. If you decided to not download the app or build it from source, just run `python main.py` and it should work the same. You will be interacting with the app through an app tray icon or a small window.
|
||||
|
||||
### Recording
|
||||
|
||||
From the app tray or GUI, you can start and stop a recording as well as pause and resume a recording. Pausing and resuming is important for when you want to hide sensitive information like credit card of login credentials. You can optionally name your recording and give it a description upon stopping a recording. You can also view your recordings by pressing the "Show Recordings" option.
|
||||
|
||||
### Playback
|
||||
|
||||
You can playback a recording, i.e. simulate the series of events from the recording, by pressing "Play Latest Recording", which plays the latest created recording, or by pressing "Play Custom Recording", which lets you choose a recording to play. You can easily replay the most recently played recording by pressing "Replay Recording".
|
||||
|
||||
To stop the app mid-playback, just press `shift`+`esc` on your keyboard.
|
||||
|
||||
### Misc
|
||||
|
||||
To quit the app, you just press the "Quit" option.
|
||||
|
||||
## Recording Format
|
||||
|
||||
Recordings are stored in `Documents/DuckTrack_Recordings`. Each recording is a directory containing:
|
||||
|
||||
1. `events.jsonl` file - sequence of all computer actions that happened. A sample event may look like this:
|
||||
```json
|
||||
{"time_stamp": 1234567.89, "action": "move", "x": 69.0, "y": 420.0}
|
||||
```
|
||||
1. `metadata.json` - stores metadata about the computer that made the recording
|
||||
2. `README.md` - stores the description for the recording
|
||||
3. MP4 file - the screen recording from OBS of the recording.
|
||||
|
||||
Here is a [sample recording](example) for further reference.
|
||||
|
||||
## Technical Overview
|
||||
|
||||
<!-- maybe put a nice graphical representation of the app here -->
|
||||
|
||||
*TDB*
|
||||
|
||||
## Known Bugs
|
||||
|
||||
- After doing lots of playbacks on macOS, a segfault will occur.
|
||||
- Mouse movement is not captured when the current application is using raw input, i.e. video games.
|
||||
- OBS may not open in the background properly on some Linux machines.
|
||||
|
||||
## Things To Do
|
||||
|
||||
- Add logging
|
||||
- Testing
|
||||
- CI (with builds and testing)
|
||||
- Add way to hide/show window from the app tray (and it saves that as a preference?)
|
||||
- Make saving preferences a thing generally, like with natural scrolling too
|
||||
|
Before Width: | Height: | Size: 6.5 KiB |
|
Before Width: | Height: | Size: 2.4 KiB |
@@ -1,27 +0,0 @@
|
||||
import shutil
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from platform import system
|
||||
from subprocess import CalledProcessError, run
|
||||
|
||||
project_dir = Path(".")
|
||||
assets_dir = project_dir / "assets"
|
||||
main_py = project_dir / "main.py"
|
||||
icon_file = assets_dir / ("duck.ico" if system() == "Windows" else "duck.png")
|
||||
|
||||
for dir_to_remove in ["dist", "build"]:
|
||||
dir_path = project_dir / dir_to_remove
|
||||
if dir_path.exists():
|
||||
shutil.rmtree(dir_path)
|
||||
|
||||
pyinstaller_cmd = [
|
||||
"pyinstaller", "--onefile", "--windowed",
|
||||
f"--add-data={assets_dir}{';' if system() == 'Windows' else ':'}{assets_dir}",
|
||||
f"--name=DuckTrack", f"--icon={icon_file}", str(main_py)
|
||||
]
|
||||
|
||||
try:
|
||||
run(pyinstaller_cmd, check=True)
|
||||
except CalledProcessError as e:
|
||||
print("An error occurred while running PyInstaller:", e)
|
||||
sys.exit(1)
|
||||
@@ -1 +0,0 @@
|
||||
from .app import MainInterface
|
||||
@@ -1,251 +0,0 @@
|
||||
import os
|
||||
import sys
|
||||
from platform import system
|
||||
|
||||
from PyQt6.QtCore import QTimer, pyqtSlot
|
||||
from PyQt6.QtGui import QAction, QIcon
|
||||
from PyQt6.QtWidgets import (QApplication, QCheckBox, QDialog, QFileDialog,
|
||||
QFormLayout, QLabel, QLineEdit, QMenu,
|
||||
QMessageBox, QPushButton, QSystemTrayIcon,
|
||||
QTextEdit, QVBoxLayout, QWidget)
|
||||
|
||||
from .obs_client import close_obs, is_obs_running, open_obs
|
||||
from .playback import Player, get_latest_recording
|
||||
from .recorder import Recorder
|
||||
from .util import get_recordings_dir, open_file
|
||||
|
||||
|
||||
class TitleDescriptionDialog(QDialog):
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
|
||||
self.setWindowTitle("Recording Details")
|
||||
|
||||
layout = QVBoxLayout(self)
|
||||
|
||||
self.form_layout = QFormLayout()
|
||||
|
||||
self.title_label = QLabel("Title:")
|
||||
self.title_input = QLineEdit(self)
|
||||
self.form_layout.addRow(self.title_label, self.title_input)
|
||||
|
||||
self.description_label = QLabel("Description:")
|
||||
self.description_input = QTextEdit(self)
|
||||
self.form_layout.addRow(self.description_label, self.description_input)
|
||||
|
||||
layout.addLayout(self.form_layout)
|
||||
|
||||
self.submit_button = QPushButton("Save", self)
|
||||
self.submit_button.clicked.connect(self.accept)
|
||||
layout.addWidget(self.submit_button)
|
||||
|
||||
def get_values(self):
|
||||
return self.title_input.text(), self.description_input.toPlainText()
|
||||
|
||||
class MainInterface(QWidget):
|
||||
def __init__(self, app: QApplication):
|
||||
super().__init__()
|
||||
self.tray = QSystemTrayIcon(QIcon(resource_path("assets/duck.png")))
|
||||
self.tray.show()
|
||||
|
||||
self.app = app
|
||||
|
||||
self.init_tray()
|
||||
self.init_window()
|
||||
|
||||
if not is_obs_running():
|
||||
self.obs_process = open_obs()
|
||||
|
||||
def init_window(self):
|
||||
self.setWindowTitle("DuckTrack")
|
||||
layout = QVBoxLayout(self)
|
||||
|
||||
self.toggle_record_button = QPushButton("Start Recording", self)
|
||||
self.toggle_record_button.clicked.connect(self.toggle_record)
|
||||
layout.addWidget(self.toggle_record_button)
|
||||
|
||||
self.toggle_pause_button = QPushButton("Pause Recording", self)
|
||||
self.toggle_pause_button.clicked.connect(self.toggle_pause)
|
||||
self.toggle_pause_button.setEnabled(False)
|
||||
layout.addWidget(self.toggle_pause_button)
|
||||
|
||||
self.show_recordings_button = QPushButton("Show Recordings", self)
|
||||
self.show_recordings_button.clicked.connect(lambda: open_file(get_recordings_dir()))
|
||||
layout.addWidget(self.show_recordings_button)
|
||||
|
||||
self.play_latest_button = QPushButton("Play Latest Recording", self)
|
||||
self.play_latest_button.clicked.connect(self.play_latest_recording)
|
||||
layout.addWidget(self.play_latest_button)
|
||||
|
||||
self.play_custom_button = QPushButton("Play Custom Recording", self)
|
||||
self.play_custom_button.clicked.connect(self.play_custom_recording)
|
||||
layout.addWidget(self.play_custom_button)
|
||||
|
||||
self.replay_recording_button = QPushButton("Replay Recording", self)
|
||||
self.replay_recording_button.clicked.connect(self.replay_recording)
|
||||
self.replay_recording_button.setEnabled(False)
|
||||
layout.addWidget(self.replay_recording_button)
|
||||
|
||||
self.quit_button = QPushButton("Quit", self)
|
||||
self.quit_button.clicked.connect(self.quit)
|
||||
layout.addWidget(self.quit_button)
|
||||
|
||||
self.natural_scrolling_checkbox = QCheckBox("Natural Scrolling", self, checked=system() == "Darwin")
|
||||
layout.addWidget(self.natural_scrolling_checkbox)
|
||||
|
||||
self.natural_scrolling_checkbox.stateChanged.connect(self.toggle_natural_scrolling)
|
||||
|
||||
self.setLayout(layout)
|
||||
|
||||
def init_tray(self):
|
||||
self.menu = QMenu()
|
||||
self.tray.setContextMenu(self.menu)
|
||||
|
||||
self.toggle_record_action = QAction("Start Recording")
|
||||
self.toggle_record_action.triggered.connect(self.toggle_record)
|
||||
self.menu.addAction(self.toggle_record_action)
|
||||
|
||||
self.toggle_pause_action = QAction("Pause Recording")
|
||||
self.toggle_pause_action.triggered.connect(self.toggle_pause)
|
||||
self.toggle_pause_action.setVisible(False)
|
||||
self.menu.addAction(self.toggle_pause_action)
|
||||
|
||||
self.show_recordings_action = QAction("Show Recordings")
|
||||
self.show_recordings_action.triggered.connect(lambda: open_file(get_recordings_dir()))
|
||||
self.menu.addAction(self.show_recordings_action)
|
||||
|
||||
self.play_latest_action = QAction("Play Latest Recording")
|
||||
self.play_latest_action.triggered.connect(self.play_latest_recording)
|
||||
self.menu.addAction(self.play_latest_action)
|
||||
|
||||
self.play_custom_action = QAction("Play Custom Recording")
|
||||
self.play_custom_action.triggered.connect(self.play_custom_recording)
|
||||
self.menu.addAction(self.play_custom_action)
|
||||
|
||||
self.replay_recording_action = QAction("Replay Recording")
|
||||
self.replay_recording_action.triggered.connect(self.replay_recording)
|
||||
self.menu.addAction(self.replay_recording_action)
|
||||
self.replay_recording_action.setVisible(False)
|
||||
|
||||
self.quit_action = QAction("Quit")
|
||||
self.quit_action.triggered.connect(self.quit)
|
||||
self.menu.addAction(self.quit_action)
|
||||
|
||||
self.menu.addSeparator()
|
||||
|
||||
self.natural_scrolling_option = QAction("Natural Scrolling", checkable=True, checked=system() == "Darwin")
|
||||
self.natural_scrolling_option.triggered.connect(self.toggle_natural_scrolling)
|
||||
self.menu.addAction(self.natural_scrolling_option)
|
||||
|
||||
@pyqtSlot()
|
||||
def replay_recording(self):
|
||||
player = Player()
|
||||
if hasattr(self, "last_played_recording_path"):
|
||||
player.play(self.last_played_recording_path)
|
||||
else:
|
||||
self.display_error_message("No recording has been played yet!")
|
||||
|
||||
@pyqtSlot()
|
||||
def play_latest_recording(self):
|
||||
player = Player()
|
||||
recording_path = get_latest_recording()
|
||||
self.last_played_recording_path = recording_path
|
||||
self.replay_recording_action.setVisible(True)
|
||||
self.replay_recording_button.setEnabled(True)
|
||||
player.play(recording_path)
|
||||
|
||||
@pyqtSlot()
|
||||
def play_custom_recording(self):
|
||||
player = Player()
|
||||
directory = QFileDialog.getExistingDirectory(None, "Select Recording", get_recordings_dir())
|
||||
if directory:
|
||||
self.last_played_recording_path = directory
|
||||
self.replay_recording_button.setEnabled(True)
|
||||
self.replay_recording_action.setVisible(True)
|
||||
player.play(directory)
|
||||
|
||||
@pyqtSlot()
|
||||
def quit(self):
|
||||
if hasattr(self, "recorder_thread"):
|
||||
self.toggle_record()
|
||||
if hasattr(self, "obs_process"):
|
||||
close_obs(self.obs_process)
|
||||
self.app.quit()
|
||||
|
||||
def closeEvent(self, event):
|
||||
self.quit()
|
||||
|
||||
@pyqtSlot()
|
||||
def toggle_natural_scrolling(self):
|
||||
sender = self.sender()
|
||||
|
||||
if sender == self.natural_scrolling_checkbox:
|
||||
state = self.natural_scrolling_checkbox.isChecked()
|
||||
self.natural_scrolling_option.setChecked(state)
|
||||
else:
|
||||
state = self.natural_scrolling_option.isChecked()
|
||||
self.natural_scrolling_checkbox.setChecked(state)
|
||||
|
||||
@pyqtSlot()
|
||||
def toggle_pause(self):
|
||||
if self.recorder_thread._is_paused:
|
||||
self.recorder_thread.resume_recording()
|
||||
self.toggle_pause_action.setText("Pause Recording")
|
||||
self.toggle_pause_button.setText("Pause Recording")
|
||||
else:
|
||||
self.recorder_thread.pause_recording()
|
||||
self.toggle_pause_action.setText("Resume Recording")
|
||||
self.toggle_pause_button.setText("Resume Recording")
|
||||
|
||||
@pyqtSlot()
|
||||
def toggle_record(self):
|
||||
if not hasattr(self, "recorder_thread"):
|
||||
self.recorder_thread = Recorder(natural_scrolling=self.natural_scrolling_checkbox.isChecked())
|
||||
self.recorder_thread.recording_stopped.connect(self.on_recording_stopped)
|
||||
self.recorder_thread.start()
|
||||
self.update_menu(True)
|
||||
else:
|
||||
self.recorder_thread.stop_recording()
|
||||
self.recorder_thread.terminate()
|
||||
|
||||
recording_dir = self.recorder_thread.recording_path
|
||||
|
||||
del self.recorder_thread
|
||||
|
||||
dialog = TitleDescriptionDialog()
|
||||
QTimer.singleShot(0, dialog.raise_)
|
||||
result = dialog.exec()
|
||||
|
||||
if result == QDialog.DialogCode.Accepted:
|
||||
title, description = dialog.get_values()
|
||||
|
||||
if title:
|
||||
renamed_dir = os.path.join(os.path.dirname(recording_dir), title)
|
||||
os.rename(recording_dir, renamed_dir)
|
||||
|
||||
with open(os.path.join(renamed_dir, 'README.md'), 'w') as f:
|
||||
f.write(description)
|
||||
|
||||
self.on_recording_stopped()
|
||||
|
||||
@pyqtSlot()
|
||||
def on_recording_stopped(self):
|
||||
self.update_menu(False)
|
||||
|
||||
def update_menu(self, is_recording: bool):
|
||||
self.toggle_record_button.setText("Stop Recording" if is_recording else "Start Recording")
|
||||
self.toggle_record_action.setText("Stop Recording" if is_recording else "Start Recording")
|
||||
|
||||
self.toggle_pause_button.setEnabled(is_recording)
|
||||
self.toggle_pause_action.setVisible(is_recording)
|
||||
|
||||
def display_error_message(self, message):
|
||||
QMessageBox.critical(None, "Error", message)
|
||||
|
||||
def resource_path(relative_path: str) -> str:
|
||||
if hasattr(sys, '_MEIPASS'):
|
||||
base_path = getattr(sys, "_MEIPASS")
|
||||
else:
|
||||
base_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), '..')
|
||||
|
||||
return os.path.join(base_path, relative_path)
|
||||
@@ -1,33 +0,0 @@
|
||||
from pynput.keyboard import Listener
|
||||
|
||||
from .util import name_to_key
|
||||
|
||||
|
||||
class KeyCombinationListener:
|
||||
"""
|
||||
Simple and bad key combination listener.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.current_keys = set()
|
||||
self.callbacks = {}
|
||||
self.listener = Listener(on_press=self.on_key_press, on_release=self.on_key_release)
|
||||
|
||||
def add_comb(self, keys, callback):
|
||||
self.callbacks[tuple([name_to_key(key_name) for key_name in sorted(keys)])] = callback
|
||||
|
||||
def on_key_press(self, key):
|
||||
self.current_keys.add(key)
|
||||
for comb, callback in self.callbacks.items():
|
||||
if all(k in self.current_keys for k in comb):
|
||||
return callback()
|
||||
|
||||
def on_key_release(self, key):
|
||||
if key in self.current_keys:
|
||||
self.current_keys.remove(key)
|
||||
|
||||
def start(self):
|
||||
self.listener.start()
|
||||
|
||||
def stop(self):
|
||||
self.listener.stop()
|
||||
@@ -1,60 +0,0 @@
|
||||
import json
|
||||
import os
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from platform import uname
|
||||
|
||||
from screeninfo import get_monitors
|
||||
|
||||
|
||||
class MetadataManager:
|
||||
"""
|
||||
Handles various system metadata collection.
|
||||
"""
|
||||
|
||||
def __init__(self, recording_path: str, natural_scrolling: bool):
|
||||
self.recording_path = recording_path
|
||||
|
||||
self.metadata = uname()._asdict()
|
||||
|
||||
self.metadata["id"] = uuid.getnode()
|
||||
|
||||
main_monitor = get_monitors()[0]
|
||||
self.metadata["screen_width"] = main_monitor.width
|
||||
self.metadata["screen_height"] = main_monitor.height
|
||||
|
||||
try:
|
||||
match self.metadata["system"]:
|
||||
case "Windows":
|
||||
import wmi
|
||||
for item in wmi.WMI().Win32_ComputerSystem():
|
||||
self.metadata["model"] = item.Model
|
||||
break
|
||||
case "Darwin":
|
||||
import subprocess
|
||||
model = subprocess.check_output(["sysctl", "-n", "hw.model"]).decode().strip()
|
||||
self.metadata["model"] = model
|
||||
case "Linux":
|
||||
with open("/sys/devices/virtual/dmi/id/product_name", "r") as f:
|
||||
self.metadata["model"] = f.read().strip()
|
||||
except:
|
||||
self.metadata["model"] = "Unknown"
|
||||
|
||||
self.metadata["scroll_direction"] = -1 if natural_scrolling else 1
|
||||
|
||||
def save_metadata(self):
|
||||
metadata_path = os.path.join(self.recording_path, "metadata.json")
|
||||
with open(metadata_path, "w") as f:
|
||||
json.dump(self.metadata, f, indent=4)
|
||||
|
||||
def collect(self):
|
||||
self.metadata["start_time"] = self._get_time_stamp()
|
||||
|
||||
def end_collect(self):
|
||||
self.metadata["stop_time"] = self._get_time_stamp()
|
||||
|
||||
def add_obs_record_state_timings(self, record_state_events: dict[str, float]):
|
||||
self.metadata["obs_record_state_timings"] = record_state_events
|
||||
|
||||
def _get_time_stamp(self):
|
||||
return datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
@@ -1,200 +0,0 @@
|
||||
import os
|
||||
import subprocess
|
||||
import time
|
||||
from platform import system
|
||||
|
||||
import obsws_python as obs
|
||||
import psutil
|
||||
|
||||
|
||||
def is_obs_running() -> bool:
|
||||
try:
|
||||
for process in psutil.process_iter(attrs=["pid", "name"]):
|
||||
if "obs" in process.info["name"].lower():
|
||||
return True
|
||||
return False
|
||||
except:
|
||||
raise Exception("Could not check if OBS is running already. Please check manually.")
|
||||
|
||||
def close_obs(obs_process: subprocess.Popen):
|
||||
if obs_process:
|
||||
obs_process.terminate()
|
||||
try:
|
||||
obs_process.wait(timeout=5)
|
||||
except subprocess.TimeoutExpired:
|
||||
obs_process.kill()
|
||||
|
||||
def find_obs() -> str:
|
||||
common_paths = {
|
||||
"Windows": [
|
||||
"C:\\Program Files\\obs-studio\\bin\\64bit\\obs64.exe",
|
||||
"C:\\Program Files (x86)\\obs-studio\\bin\\32bit\\obs32.exe"
|
||||
],
|
||||
"Darwin": [
|
||||
"/Applications/OBS.app/Contents/MacOS/OBS",
|
||||
"/opt/homebrew/bin/obs"
|
||||
],
|
||||
"Linux": [
|
||||
"/usr/bin/obs",
|
||||
"/usr/local/bin/obs"
|
||||
]
|
||||
}
|
||||
|
||||
for path in common_paths.get(system(), []):
|
||||
if os.path.exists(path):
|
||||
return path
|
||||
|
||||
try:
|
||||
if system() == "Windows":
|
||||
obs_path = subprocess.check_output("where obs", shell=True).decode().strip()
|
||||
else:
|
||||
obs_path = subprocess.check_output("which obs", shell=True).decode().strip()
|
||||
|
||||
if os.path.exists(obs_path):
|
||||
return obs_path
|
||||
except subprocess.CalledProcessError:
|
||||
pass
|
||||
|
||||
return "obs"
|
||||
|
||||
def open_obs() -> subprocess.Popen:
|
||||
try:
|
||||
obs_path = find_obs()
|
||||
if system() == "Windows":
|
||||
# you have to change the working directory first for OBS to find the correct locale on windows
|
||||
os.chdir(os.path.dirname(obs_path))
|
||||
obs_path = os.path.basename(obs_path)
|
||||
return subprocess.Popen([obs_path, "--startreplaybuffer", "--minimize-to-tray"])
|
||||
except:
|
||||
raise Exception("Failed to find OBS, please open OBS manually.")
|
||||
|
||||
class OBSClient:
|
||||
"""
|
||||
Controls the OBS client via the OBS websocket.
|
||||
Sets all the correct settings for recording.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
recording_path: str,
|
||||
metadata: dict,
|
||||
fps=30,
|
||||
output_width=1280,
|
||||
output_height=720,
|
||||
):
|
||||
self.metadata = metadata
|
||||
|
||||
self.req_client = obs.ReqClient()
|
||||
self.event_client = obs.EventClient()
|
||||
|
||||
self.record_state_events = {}
|
||||
|
||||
def on_record_state_changed(data):
|
||||
output_state = data.output_state
|
||||
print("record state changed:", output_state)
|
||||
if output_state not in self.record_state_events:
|
||||
self.record_state_events[output_state] = []
|
||||
self.record_state_events[output_state].append(time.perf_counter())
|
||||
|
||||
self.event_client.callback.register(on_record_state_changed)
|
||||
|
||||
self.old_profile = self.req_client.get_profile_list().current_profile_name
|
||||
|
||||
if "computer_tracker" not in self.req_client.get_profile_list().profiles:
|
||||
self.req_client.create_profile("computer_tracker")
|
||||
else:
|
||||
self.req_client.set_current_profile("computer_tracker")
|
||||
self.req_client.create_profile("temp")
|
||||
self.req_client.remove_profile("temp")
|
||||
self.req_client.set_current_profile("computer_tracker")
|
||||
|
||||
base_width = metadata["screen_width"]
|
||||
base_height = metadata["screen_height"]
|
||||
|
||||
if metadata["system"] == "Darwin":
|
||||
# for retina displays
|
||||
# TODO: check if external displays are messed up by this
|
||||
base_width *= 2
|
||||
base_height *= 2
|
||||
|
||||
scaled_width, scaled_height = _scale_resolution(base_width, base_height, output_width, output_height)
|
||||
|
||||
self.req_client.set_profile_parameter("Video", "BaseCX", str(base_width))
|
||||
self.req_client.set_profile_parameter("Video", "BaseCY", str(base_height))
|
||||
self.req_client.set_profile_parameter("Video", "OutputCX", str(scaled_width))
|
||||
self.req_client.set_profile_parameter("Video", "OutputCY", str(scaled_height))
|
||||
self.req_client.set_profile_parameter("Video", "ScaleType", "lanczos")
|
||||
|
||||
self.req_client.set_profile_parameter("AdvOut", "RescaleRes", f"{base_width}x{base_height}")
|
||||
self.req_client.set_profile_parameter("AdvOut", "RecRescaleRes", f"{base_width}x{base_height}")
|
||||
self.req_client.set_profile_parameter("AdvOut", "FFRescaleRes", f"{base_width}x{base_height}")
|
||||
|
||||
self.req_client.set_profile_parameter("Video", "FPSCommon", str(fps))
|
||||
self.req_client.set_profile_parameter("Video", "FPSInt", str(fps))
|
||||
self.req_client.set_profile_parameter("Video", "FPSNum", str(fps))
|
||||
self.req_client.set_profile_parameter("Video", "FPSDen", "1")
|
||||
|
||||
self.req_client.set_profile_parameter("SimpleOutput", "RecFormat2", "mp4")
|
||||
|
||||
bitrate = int(_get_bitrate_mbps(scaled_width, scaled_height, fps=fps) * 1000 / 50) * 50
|
||||
self.req_client.set_profile_parameter("SimpleOutput", "VBitrate", str(bitrate))
|
||||
|
||||
# do this in order to get pause & resume
|
||||
self.req_client.set_profile_parameter("SimpleOutput", "RecQuality", "Small")
|
||||
|
||||
self.req_client.set_profile_parameter("SimpleOutput", "FilePath", recording_path)
|
||||
|
||||
# TODO: not all OBS configs have this, maybe just instruct the user to mute themselves
|
||||
|
||||
|
||||
try:
|
||||
self.req_client.set_input_mute("Mic/Aux", muted=True)
|
||||
except obs.error.OBSSDKRequestError :
|
||||
# In case there is no Mic/Aux input, this will throw an error
|
||||
pass
|
||||
|
||||
def start_recording(self):
|
||||
self.req_client.start_record()
|
||||
|
||||
def stop_recording(self):
|
||||
self.req_client.stop_record()
|
||||
self.req_client.set_current_profile(self.old_profile) # restore old profile
|
||||
|
||||
def pause_recording(self):
|
||||
self.req_client.pause_record()
|
||||
|
||||
def resume_recording(self):
|
||||
self.req_client.resume_record()
|
||||
|
||||
def _get_bitrate_mbps(width: int, height: int, fps=30) -> float:
|
||||
"""
|
||||
Gets the YouTube recommended bitrate in Mbps for a given resolution and framerate.
|
||||
Refer to https://support.google.com/youtube/answer/1722171?hl=en#zippy=%2Cbitrate
|
||||
"""
|
||||
resolutions = {
|
||||
(7680, 4320): {30: 120, 60: 180},
|
||||
(3840, 2160): {30: 40, 60: 60.5},
|
||||
(2160, 1440): {30: 16, 60: 24},
|
||||
(1920, 1080): {30: 8, 60: 12},
|
||||
(1280, 720): {30: 5, 60: 7.5},
|
||||
(640, 480): {30: 2.5, 60: 4},
|
||||
(480, 360): {30: 1, 60: 1.5}
|
||||
}
|
||||
|
||||
if (width, height) in resolutions:
|
||||
return resolutions[(width, height)].get(fps)
|
||||
else:
|
||||
# approximate the bitrate using a simple linear model
|
||||
area = width * height
|
||||
multiplier = 3.5982188179592543e-06 if fps == 30 else 5.396175171097084e-06
|
||||
constant = 2.418399836285939 if fps == 30 else 3.742780056500365
|
||||
return multiplier * area + constant
|
||||
|
||||
def _scale_resolution(base_width: int, base_height: int, target_width: int, target_height: int) -> tuple[int, int]:
|
||||
target_area = target_width * target_height
|
||||
aspect_ratio = base_width / base_height
|
||||
|
||||
scaled_height = int((target_area / aspect_ratio) ** 0.5)
|
||||
scaled_width = int(aspect_ratio * scaled_height)
|
||||
|
||||
return scaled_width, scaled_height
|
||||
@@ -1,188 +0,0 @@
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
|
||||
import pyautogui
|
||||
from pynput.keyboard import Controller as KeyboardController
|
||||
from pynput.keyboard import Key
|
||||
from pynput.mouse import Button
|
||||
from pynput.mouse import Controller as MouseController
|
||||
|
||||
from .keycomb import KeyCombinationListener
|
||||
from .util import (fix_windows_dpi_scaling, get_recordings_dir, name_to_button,
|
||||
name_to_key)
|
||||
|
||||
pyautogui.PAUSE = 0
|
||||
pyautogui.DARWIN_CATCH_UP_TIME = 0
|
||||
|
||||
class Player:
|
||||
"""
|
||||
Plays back recordings.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.stop_playback = False
|
||||
self.listener = KeyCombinationListener()
|
||||
|
||||
def stop_comb_pressed():
|
||||
self.stop_playback = True
|
||||
return False
|
||||
|
||||
self.listener.add_comb(("shift", "esc"), stop_comb_pressed)
|
||||
self.listener.start()
|
||||
|
||||
def play(self, recording_path: str):
|
||||
with open(os.path.join(recording_path, "events.jsonl"), "r") as f:
|
||||
events = [json.loads(line) for line in f.readlines()]
|
||||
|
||||
with open(os.path.join(recording_path, "metadata.json"), "r") as f:
|
||||
metadata = json.load(f)
|
||||
|
||||
self.playback(events, metadata)
|
||||
|
||||
def playback(self, events: list[dict], metadata: dict):
|
||||
if metadata["system"] == "Windows":
|
||||
fix_windows_dpi_scaling()
|
||||
|
||||
mouse_controller = MouseController()
|
||||
keyboard_controller = KeyboardController()
|
||||
|
||||
if not events:
|
||||
self.listener.stop()
|
||||
return
|
||||
|
||||
presses_to_skip = 0
|
||||
releases_to_skip = 0
|
||||
|
||||
in_click_sequence = False
|
||||
|
||||
for i, event in enumerate(events):
|
||||
start_time = time.perf_counter()
|
||||
|
||||
if self.stop_playback:
|
||||
return
|
||||
|
||||
def do_mouse_press(button):
|
||||
for j, second_event in enumerate(events[i+1:]):
|
||||
# make sure the time between mouse clicks is less than 500ms
|
||||
if second_event["time_stamp"] - event["time_stamp"] > 0.5:
|
||||
break
|
||||
|
||||
if "x" in second_event and "y" in second_event:
|
||||
# if the mouse moves out of the click radius/rectangle, it is not a click sequence
|
||||
if math.sqrt((second_event["y"] - event["y"]) ** 2 +
|
||||
(second_event["x"] - event["x"]) ** 2) > 4:
|
||||
break
|
||||
|
||||
if second_event["action"] == "click" and second_event["pressed"]:
|
||||
for k, third_event in enumerate(events[i+j+2:]):
|
||||
if third_event["time_stamp"] - second_event["time_stamp"] > 0.5:
|
||||
break
|
||||
|
||||
if "x" in third_event and "y" in third_event:
|
||||
if math.sqrt((third_event["y"] - event["y"]) ** 2 +
|
||||
(third_event["x"] - event["x"]) ** 2) > 5:
|
||||
break
|
||||
|
||||
if third_event["action"] == "click" and third_event["pressed"]:
|
||||
mouse_controller.click(button, 3)
|
||||
return 2, 2
|
||||
|
||||
mouse_controller.click(button, 2)
|
||||
return 1, 1
|
||||
|
||||
mouse_controller.press(button)
|
||||
return 0, 0
|
||||
|
||||
if event["action"] == "move":
|
||||
mouse_controller.position = (event["x"], event["y"])
|
||||
|
||||
elif event["action"] == "click":
|
||||
button = name_to_button(event["button"])
|
||||
|
||||
if event["pressed"]:
|
||||
if presses_to_skip == 0:
|
||||
presses, releases = do_mouse_press(button)
|
||||
presses_to_skip += presses
|
||||
releases_to_skip += releases
|
||||
|
||||
if presses > 0:
|
||||
in_click_sequence = True
|
||||
else:
|
||||
presses_to_skip -= 1
|
||||
else:
|
||||
if releases_to_skip == 0:
|
||||
mouse_controller.release(button)
|
||||
|
||||
if in_click_sequence:
|
||||
keyboard_controller.press(Key.shift)
|
||||
mouse_controller.click(Button.left)
|
||||
keyboard_controller.release(Key.shift)
|
||||
in_click_sequence = False
|
||||
else:
|
||||
releases_to_skip -= 1
|
||||
|
||||
elif event["action"] == "scroll":
|
||||
if metadata["system"] == "Windows":
|
||||
# for some reason on windows, pynput scroll is correct but pyautogui is not
|
||||
mouse_controller.scroll(metadata["scroll_direction"] * event["dx"], metadata["scroll_direction"] * event["dy"])
|
||||
else:
|
||||
pyautogui.hscroll(clicks=metadata["scroll_direction"] * event["dx"])
|
||||
pyautogui.vscroll(clicks=metadata["scroll_direction"] * event["dy"])
|
||||
|
||||
elif event["action"] in ["press", "release"]:
|
||||
key = name_to_key(event["name"])
|
||||
if event["action"] == "press":
|
||||
keyboard_controller.press(key)
|
||||
else:
|
||||
keyboard_controller.release(key)
|
||||
|
||||
# sleep for the correct amount of time
|
||||
|
||||
end_time = time.perf_counter()
|
||||
execution_time = end_time - start_time
|
||||
|
||||
if i + 1 < len(events):
|
||||
desired_delay = events[i + 1]["time_stamp"] - event["time_stamp"]
|
||||
delay = desired_delay - execution_time
|
||||
if delay < 0:
|
||||
print(f"warning: behind by {-delay * 1000:.3f} ms")
|
||||
elif delay != 0:
|
||||
wait_until = time.perf_counter() + delay
|
||||
while time.perf_counter() < wait_until:
|
||||
pass
|
||||
|
||||
self.listener.stop()
|
||||
|
||||
def get_latest_recording() -> str:
|
||||
recordings_dir = get_recordings_dir()
|
||||
if not os.path.exists(recordings_dir):
|
||||
raise Exception("The recordings directory does not exist")
|
||||
|
||||
recordings = [os.path.join(recordings_dir, f) for f in os.listdir(recordings_dir) if os.path.isdir(os.path.join(recordings_dir, f))]
|
||||
|
||||
if len(recordings) == 0:
|
||||
raise Exception("You have no recordings to play back")
|
||||
|
||||
latest_recording = max(recordings, key=os.path.getctime)
|
||||
|
||||
return latest_recording
|
||||
|
||||
def main():
|
||||
player = Player()
|
||||
|
||||
if len(sys.argv) > 1:
|
||||
recording_path = sys.argv[1]
|
||||
else:
|
||||
recording_path = get_latest_recording()
|
||||
|
||||
player.play(recording_path)
|
||||
|
||||
if __name__ == "__main__":
|
||||
n = 3
|
||||
print("press shift+esc to stop the playback")
|
||||
print(f"starting in {n} seconds...")
|
||||
time.sleep(n)
|
||||
main()
|
||||
@@ -1,145 +0,0 @@
|
||||
import json
|
||||
import os
|
||||
import time
|
||||
from datetime import datetime
|
||||
from platform import system
|
||||
from queue import Queue
|
||||
|
||||
from pynput import keyboard, mouse
|
||||
from pynput.keyboard import KeyCode
|
||||
from PyQt6.QtCore import QThread, pyqtSignal
|
||||
|
||||
from .metadata import MetadataManager
|
||||
from .obs_client import OBSClient
|
||||
from .util import fix_windows_dpi_scaling, get_recordings_dir
|
||||
|
||||
|
||||
class Recorder(QThread):
|
||||
"""
|
||||
Makes recordings.
|
||||
"""
|
||||
|
||||
recording_stopped = pyqtSignal()
|
||||
|
||||
def __init__(self, natural_scrolling: bool):
|
||||
super().__init__()
|
||||
|
||||
if system() == "Windows":
|
||||
fix_windows_dpi_scaling()
|
||||
|
||||
self.recording_path = self._get_recording_path()
|
||||
|
||||
self._is_recording = False
|
||||
self._is_paused = False
|
||||
|
||||
self.event_queue = Queue()
|
||||
self.events_file = open(os.path.join(self.recording_path, "events.jsonl"), "a")
|
||||
|
||||
self.metadata_manager = MetadataManager(
|
||||
recording_path=self.recording_path,
|
||||
natural_scrolling=natural_scrolling
|
||||
)
|
||||
self.obs_client = OBSClient(recording_path=self.recording_path,
|
||||
metadata=self.metadata_manager.metadata)
|
||||
|
||||
self.mouse_listener = mouse.Listener(
|
||||
on_move=self.on_move,
|
||||
on_click=self.on_click,
|
||||
on_scroll=self.on_scroll)
|
||||
|
||||
self.keyboard_listener = keyboard.Listener(
|
||||
on_press=self.on_press,
|
||||
on_release=self.on_release)
|
||||
|
||||
def on_move(self, x, y):
|
||||
if not self._is_paused:
|
||||
self.event_queue.put({"time_stamp": time.perf_counter(),
|
||||
"action": "move",
|
||||
"x": x,
|
||||
"y": y}, block=False)
|
||||
|
||||
def on_click(self, x, y, button, pressed):
|
||||
if not self._is_paused:
|
||||
self.event_queue.put({"time_stamp": time.perf_counter(),
|
||||
"action": "click",
|
||||
"x": x,
|
||||
"y": y,
|
||||
"button": button.name,
|
||||
"pressed": pressed}, block=False)
|
||||
|
||||
def on_scroll(self, x, y, dx, dy):
|
||||
if not self._is_paused:
|
||||
self.event_queue.put({"time_stamp": time.perf_counter(),
|
||||
"action": "scroll",
|
||||
"x": x,
|
||||
"y": y,
|
||||
"dx": dx,
|
||||
"dy": dy}, block=False)
|
||||
|
||||
def on_press(self, key):
|
||||
if not self._is_paused:
|
||||
self.event_queue.put({"time_stamp": time.perf_counter(),
|
||||
"action": "press",
|
||||
"name": key.char if type(key) == KeyCode else key.name}, block=False)
|
||||
|
||||
def on_release(self, key):
|
||||
if not self._is_paused:
|
||||
self.event_queue.put({"time_stamp": time.perf_counter(),
|
||||
"action": "release",
|
||||
"name": key.char if type(key) == KeyCode else key.name}, block=False)
|
||||
|
||||
def run(self):
|
||||
self._is_recording = True
|
||||
|
||||
self.metadata_manager.collect()
|
||||
self.obs_client.start_recording()
|
||||
|
||||
self.mouse_listener.start()
|
||||
self.keyboard_listener.start()
|
||||
|
||||
while self._is_recording:
|
||||
event = self.event_queue.get()
|
||||
self.events_file.write(json.dumps(event) + "\n")
|
||||
|
||||
def stop_recording(self):
|
||||
if self._is_recording:
|
||||
self._is_recording = False
|
||||
|
||||
self.metadata_manager.end_collect()
|
||||
|
||||
self.mouse_listener.stop()
|
||||
self.keyboard_listener.stop()
|
||||
|
||||
self.obs_client.stop_recording()
|
||||
self.metadata_manager.add_obs_record_state_timings(self.obs_client.record_state_events)
|
||||
self.events_file.close()
|
||||
self.metadata_manager.save_metadata()
|
||||
|
||||
self.recording_stopped.emit()
|
||||
|
||||
def pause_recording(self):
|
||||
if not self._is_paused and self._is_recording:
|
||||
self._is_paused = True
|
||||
self.obs_client.pause_recording()
|
||||
self.event_queue.put({"time_stamp": time.perf_counter(),
|
||||
"action": "pause"}, block=False)
|
||||
|
||||
def resume_recording(self):
|
||||
if self._is_paused and self._is_recording:
|
||||
self._is_paused = False
|
||||
self.obs_client.resume_recording()
|
||||
self.event_queue.put({"time_stamp": time.perf_counter(),
|
||||
"action": "resume"}, block=False)
|
||||
|
||||
def _get_recording_path(self) -> str:
|
||||
recordings_dir = get_recordings_dir()
|
||||
|
||||
if not os.path.exists(recordings_dir):
|
||||
os.mkdir(recordings_dir)
|
||||
|
||||
current_time = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
|
||||
|
||||
recording_path = os.path.join(recordings_dir, f"recording-{current_time}")
|
||||
os.mkdir(recording_path)
|
||||
|
||||
return recording_path
|
||||
@@ -1,38 +0,0 @@
|
||||
import os
|
||||
import platform
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
from pynput.keyboard import Key, KeyCode
|
||||
from pynput.mouse import Button
|
||||
|
||||
|
||||
def name_to_key(name: str) -> Key | KeyCode:
|
||||
try:
|
||||
return getattr(Key, name)
|
||||
except AttributeError:
|
||||
return KeyCode.from_char(name)
|
||||
|
||||
def name_to_button(name: str) -> Button:
|
||||
return getattr(Button, name)
|
||||
|
||||
def get_recordings_dir() -> str:
|
||||
documents_folder = Path.home() / 'Documents' / 'DuckTrack_Recordings'
|
||||
return str(documents_folder)
|
||||
|
||||
def fix_windows_dpi_scaling():
|
||||
"""
|
||||
Fixes DPI scaling issues with legacy windows applications
|
||||
Reference: https://pynput.readthedocs.io/en/latest/mouse.html#ensuring-consistent-coordinates-between-listener-and-controller-on-windows
|
||||
"""
|
||||
import ctypes
|
||||
PROCESS_PER_MONITOR_DPI_AWARE = 2
|
||||
ctypes.windll.shcore.SetProcessDpiAwareness(PROCESS_PER_MONITOR_DPI_AWARE)
|
||||
|
||||
def open_file(path):
|
||||
if platform.system() == "Windows":
|
||||
os.startfile(path)
|
||||
elif platform.system() == "Darwin":
|
||||
subprocess.Popen(["open", path])
|
||||
else:
|
||||
subprocess.Popen(["xdg-open", path])
|
||||
@@ -1,48 +0,0 @@
|
||||
import glob
|
||||
|
||||
import matplotlib.pyplot as plt
|
||||
import numpy as np
|
||||
import seaborn as sns
|
||||
from scipy.stats import sem, t
|
||||
|
||||
|
||||
def calculate_confidence_interval(data, confidence=0.95):
|
||||
n = len(data)
|
||||
m = np.mean(data)
|
||||
std_err = sem(data)
|
||||
h = std_err * t.ppf((1 + confidence) / 2, n - 1)
|
||||
return m, m-h, m+h
|
||||
|
||||
runs = glob.glob("run*.txt")
|
||||
TOTAL_EVENTS = 22509
|
||||
percent_delays = []
|
||||
all_delays = []
|
||||
|
||||
for run in runs:
|
||||
with open(run, "r") as f:
|
||||
delays = [float(line.split()[3]) for line in f if float(line.split()[3]) > 0] # consider only positive delays
|
||||
percent_delays.append((len(delays) / TOTAL_EVENTS) * 100)
|
||||
all_delays.extend(delays)
|
||||
|
||||
average_percent_delays = np.mean(percent_delays)
|
||||
confidence_interval_percent_delays = calculate_confidence_interval(percent_delays)
|
||||
print(f"Average percentage of delayed events across all runs: {average_percent_delays:.2f}%")
|
||||
print(f"95% Confidence interval: ({confidence_interval_percent_delays[1]:.2f}%, {confidence_interval_percent_delays[2]:.2f}%)")
|
||||
|
||||
if all_delays:
|
||||
mean_delay = np.mean(all_delays)
|
||||
confidence_interval_delays = calculate_confidence_interval(all_delays)
|
||||
print(f"Mean delay time: {mean_delay:.2f}")
|
||||
print(f"95% Confidence interval for delay time: ({confidence_interval_delays[1]:.2f}, {confidence_interval_delays[2]:.2f})")
|
||||
else:
|
||||
print("No delay data available for calculation.")
|
||||
|
||||
sns.histplot(all_delays, bins=30, kde=False)
|
||||
plt.xlabel('Delay Time (ms)')
|
||||
plt.ylabel('Frequency')
|
||||
plt.yscale('log')
|
||||
plt.title('Histogram of Delay Times (macOS)')
|
||||
|
||||
plt.savefig('delays.png', dpi=300)
|
||||
|
||||
plt.show()
|
||||
@@ -1,110 +0,0 @@
|
||||
import glob
|
||||
import os
|
||||
|
||||
import cv2
|
||||
import matplotlib.pyplot as plt
|
||||
import numpy as np
|
||||
import scipy.stats as stats
|
||||
from skimage.metrics import structural_similarity as ssim
|
||||
from tqdm import tqdm
|
||||
|
||||
# use this: https://sketch.io
|
||||
|
||||
def calculate_rmse(imageA, imageB):
|
||||
err = np.sum((imageA - imageB) ** 2)
|
||||
err /= float(imageA.shape[0] * imageA.shape[1])
|
||||
return np.sqrt(err)
|
||||
|
||||
def compare_images(ground_truth_path, sample_paths):
|
||||
results = []
|
||||
gt_image = cv2.imread(ground_truth_path, cv2.IMREAD_GRAYSCALE)
|
||||
|
||||
if gt_image is None:
|
||||
raise ValueError("Ground truth image could not be read. Please check the file path.")
|
||||
|
||||
gt_image = gt_image.astype("float") / 255.0
|
||||
|
||||
for path in tqdm(sample_paths):
|
||||
sample_image = cv2.imread(path, cv2.IMREAD_GRAYSCALE)
|
||||
|
||||
if sample_image is None:
|
||||
print(f"WARNING: Sample image at path {path} could not be read. Skipping this image.")
|
||||
continue
|
||||
|
||||
sample_image = sample_image.astype("float") / 255.0
|
||||
|
||||
rmse_value = calculate_rmse(gt_image, sample_image)
|
||||
ssim_value, _ = ssim(gt_image, sample_image, full=True, data_range=1) # Corrected line
|
||||
|
||||
diff_mask = cv2.absdiff(gt_image, sample_image)
|
||||
|
||||
# plt.imshow(diff_mask * 255, cmap='gray')
|
||||
# plt.title(f'Difference Mask for {os.path.basename(path)}\nRMSE: {rmse_value:.5f} - SSIM: {ssim_value:.5f}')
|
||||
# plt.show()
|
||||
|
||||
results.append({
|
||||
'path': path,
|
||||
'rmse': rmse_value,
|
||||
'ssim': ssim_value,
|
||||
'diff_mask': diff_mask
|
||||
})
|
||||
|
||||
return results
|
||||
|
||||
|
||||
ground_truth = 'ground_truth.png'
|
||||
sample_images = glob.glob("samples/*.png")
|
||||
|
||||
results = compare_images(ground_truth, sample_images)
|
||||
|
||||
for res in results:
|
||||
print(f"Image: {res['path']} - RMSE: {res['rmse']} - SSIM: {res['ssim']}")
|
||||
|
||||
def calculate_confidence_interval(data, confidence_level=0.95):
|
||||
mean = np.mean(data)
|
||||
sem = stats.sem(data)
|
||||
df = len(data) - 1
|
||||
me = sem * stats.t.ppf((1 + confidence_level) / 2, df)
|
||||
return mean - me, mean + me
|
||||
|
||||
rmse_values = [res['rmse'] for res in results]
|
||||
ssim_values = [res['ssim'] for res in results]
|
||||
|
||||
rmse_mean = np.mean(rmse_values)
|
||||
rmse_median = np.median(rmse_values)
|
||||
rmse_stdev = np.std(rmse_values, ddof=1)
|
||||
|
||||
ssim_mean = np.mean(ssim_values)
|
||||
ssim_median = np.median(ssim_values)
|
||||
ssim_stdev = np.std(ssim_values, ddof=1)
|
||||
|
||||
rmse_ci = calculate_confidence_interval(rmse_values)
|
||||
ssim_ci = calculate_confidence_interval(ssim_values)
|
||||
|
||||
print(f"\nRMSE - Mean: {rmse_mean}, Median: {rmse_median}, Std Dev: {rmse_stdev}, 95% CI: {rmse_ci}")
|
||||
print(f"SSIM - Mean: {ssim_mean}, Median: {ssim_median}, Std Dev: {ssim_stdev}, 95% CI: {ssim_ci}")
|
||||
|
||||
print(f"RMSE: {rmse_mean} ± {rmse_ci[1] - rmse_mean}")
|
||||
print(f"SSIM: {ssim_mean} ± {ssim_ci[1] - ssim_mean}")
|
||||
|
||||
def save_average_diff_map(results, save_path='average_diff_map.png'):
|
||||
if not results:
|
||||
print("No results available to create an average diff map.")
|
||||
return
|
||||
|
||||
avg_diff_map = None
|
||||
|
||||
for res in results:
|
||||
if avg_diff_map is None:
|
||||
avg_diff_map = np.zeros_like(res['diff_mask'])
|
||||
|
||||
avg_diff_map += res['diff_mask']
|
||||
|
||||
avg_diff_map /= len(results)
|
||||
|
||||
avg_diff_map = (avg_diff_map * 255).astype(np.uint8)
|
||||
|
||||
cv2.imwrite(save_path, avg_diff_map)
|
||||
|
||||
# Usage
|
||||
save_average_diff_map(results)
|
||||
@@ -1,4 +0,0 @@
|
||||
success = 10
|
||||
total = 10
|
||||
|
||||
print(success / total)
|
||||
@@ -1,48 +0,0 @@
|
||||
import csv
|
||||
import time
|
||||
|
||||
import numpy as np
|
||||
from tqdm import tqdm
|
||||
|
||||
|
||||
def check_sleep(duration, sleep_function):
|
||||
start = time.perf_counter()
|
||||
sleep_function(duration)
|
||||
end = time.perf_counter()
|
||||
elapsed = end - start
|
||||
return abs(elapsed - duration)
|
||||
|
||||
def busy_sleep(duration):
|
||||
end_time = time.perf_counter() + duration
|
||||
while time.perf_counter() < end_time:
|
||||
pass
|
||||
|
||||
def measure_accuracy(sleep_function, durations, iterations=100):
|
||||
average_errors = []
|
||||
for duration in tqdm(durations):
|
||||
errors = [check_sleep(duration, sleep_function) for _ in range(iterations)]
|
||||
average_error = np.mean(errors)
|
||||
average_errors.append(average_error)
|
||||
return average_errors
|
||||
|
||||
durations = np.arange(0.001, 0.101, 0.001) # From 1ms to 100ms in 1ms increments
|
||||
iterations = 100
|
||||
|
||||
sleep_errors = measure_accuracy(time.sleep, durations, iterations)
|
||||
busy_sleep_errors = measure_accuracy(busy_sleep, durations, iterations)
|
||||
|
||||
def save_to_csv(filename, durations, sleep_errors, busy_sleep_errors):
|
||||
with open(filename, 'w', newline='') as csvfile:
|
||||
fieldnames = ['duration', 'sleep_error', 'busy_sleep_error']
|
||||
writer = csv.DictWriter(csvfile, fieldnames=fieldnames)
|
||||
|
||||
writer.writeheader()
|
||||
for duration, sleep_error, busy_sleep_error in zip(durations, sleep_errors, busy_sleep_errors):
|
||||
writer.writerow({
|
||||
'duration': duration,
|
||||
'sleep_error': sleep_error,
|
||||
'busy_sleep_error': busy_sleep_error
|
||||
})
|
||||
print("Data saved to", filename)
|
||||
|
||||
save_to_csv('sleep_data.csv', durations * 1000, np.array(sleep_errors) * 1000, np.array(busy_sleep_errors) * 1000)
|
||||
@@ -1,33 +0,0 @@
|
||||
import csv
|
||||
|
||||
import matplotlib.pyplot as plt
|
||||
|
||||
|
||||
def plot_from_csv(filename, save_plot=False):
|
||||
durations = []
|
||||
sleep_errors = []
|
||||
busy_sleep_errors = []
|
||||
|
||||
with open(filename, 'r') as csvfile:
|
||||
reader = csv.DictReader(csvfile)
|
||||
for row in reader:
|
||||
durations.append(float(row['duration']))
|
||||
sleep_errors.append(float(row['sleep_error']))
|
||||
busy_sleep_errors.append(float(row['busy_sleep_error']))
|
||||
|
||||
plt.figure(figsize=(10, 5))
|
||||
plt.plot(durations, sleep_errors, label='time.sleep()', marker='o')
|
||||
plt.plot(durations, busy_sleep_errors, label='busy_sleep()', marker='x')
|
||||
plt.xlabel('Desired Delay (ms)')
|
||||
plt.ylabel('Average Error (ms)')
|
||||
plt.title('Sleep Accuracy: time.sleep() vs Busy-Wait Loop (macOS)')
|
||||
plt.legend()
|
||||
plt.grid(True)
|
||||
|
||||
if save_plot:
|
||||
plt.savefig('sleep_accuracy_plot.png', dpi=300)
|
||||
print("Plot saved as sleep_accuracy_plot.png")
|
||||
|
||||
plt.show()
|
||||
|
||||
plot_from_csv('sleep_data.csv', save_plot=True)
|
||||
@@ -1,110 +0,0 @@
|
||||
import glob
|
||||
|
||||
import matplotlib.pyplot as plt
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
import scipy.stats as stats
|
||||
import seaborn as sns
|
||||
|
||||
# use this: https://www.estopwatch.net/
|
||||
|
||||
def read_file(file_path):
|
||||
df = pd.read_csv(file_path)
|
||||
df['Elapsed time'] = pd.to_datetime(df['Elapsed time'], errors='coerce')
|
||||
return df
|
||||
|
||||
|
||||
def analyze_new_error(run_df, groundtruth_df):
|
||||
cumulative_errors = run_df['Elapsed time'] - groundtruth_df['Elapsed time']
|
||||
cumulative_errors_in_seconds = cumulative_errors.dt.total_seconds()
|
||||
|
||||
new_errors_in_seconds = cumulative_errors_in_seconds.diff().fillna(cumulative_errors_in_seconds[0])
|
||||
new_error_points = new_errors_in_seconds[new_errors_in_seconds != 0].index.tolist()
|
||||
|
||||
return new_errors_in_seconds[new_error_points]
|
||||
|
||||
def calculate_statistics(errors):
|
||||
if len(errors) == 0:
|
||||
return {
|
||||
'mean_error': 0,
|
||||
'median_error': 0,
|
||||
'stddev_error': 0,
|
||||
'rmse_error': 0,
|
||||
'confidence_interval': (0, 0),
|
||||
'error_frequency': 0
|
||||
}
|
||||
|
||||
mean_error = np.mean(errors)
|
||||
median_error = np.median(errors)
|
||||
stddev_error = np.std(errors)
|
||||
rmse_error = np.sqrt(np.mean(np.square(errors)))
|
||||
|
||||
ci_low, ci_high = stats.t.interval(
|
||||
confidence=0.95,
|
||||
df=len(errors) - 1,
|
||||
loc=mean_error,
|
||||
scale=stats.sem(errors) if len(errors) > 1 else 0
|
||||
)
|
||||
|
||||
return {
|
||||
'mean_error': mean_error,
|
||||
'median_error': median_error,
|
||||
'stddev_error': stddev_error,
|
||||
'rmse_error': rmse_error,
|
||||
'confidence_interval': (ci_low, ci_high),
|
||||
}
|
||||
|
||||
|
||||
def main():
|
||||
groundtruth_file = 'groundtruth.csv'
|
||||
run_files = glob.glob('runs/*.csv')
|
||||
|
||||
groundtruth_df = read_file(groundtruth_file)
|
||||
run_dfs = {f'run{i+1}': read_file(file) for i, file in enumerate(run_files)}
|
||||
|
||||
total_errors = []
|
||||
total_points = 0
|
||||
all_errors = []
|
||||
|
||||
for run, df in run_dfs.items():
|
||||
errors = analyze_new_error(df, groundtruth_df)
|
||||
total_errors.extend(errors)
|
||||
all_errors.extend(errors)
|
||||
total_points += len(df)
|
||||
|
||||
results = calculate_statistics(errors)
|
||||
error_frequency = len(errors) / len(df)
|
||||
|
||||
print(f"Results for {run}:")
|
||||
print(f"Mean New Error: {results['mean_error']:.5f} seconds")
|
||||
print(f"Median New Error: {results['median_error']:.5f} seconds")
|
||||
print(f"Standard Deviation of New Error: {results['stddev_error']:.5f} seconds")
|
||||
print(f"RMSE of New Error: {results['rmse_error']:.5f} seconds")
|
||||
print(f"95% Confidence Interval of New Error: ({results['confidence_interval'][0]:.5f}, {results['confidence_interval'][1]:.5f}) seconds")
|
||||
print(f"New Error Frequency: {error_frequency*100:.5f} %")
|
||||
print('-----------------------------------------')
|
||||
|
||||
total_results = calculate_statistics(total_errors)
|
||||
total_error_frequency = len(total_errors) / total_points
|
||||
|
||||
print("Total Statistics:")
|
||||
print(f"Mean New Error: {total_results['mean_error']:.5f} seconds")
|
||||
print(f"Median New Error: {total_results['median_error']:.5f} seconds")
|
||||
print(f"Standard Deviation of New Error: {total_results['stddev_error']:.5f} seconds")
|
||||
print(f"RMSE of New Error: {total_results['rmse_error']:.5f} seconds")
|
||||
print(f"95% Confidence Interval of New Error: ({total_results['confidence_interval'][0]:.5f}, {total_results['confidence_interval'][1]:.5f}) seconds")
|
||||
print(f"New Error Frequency: {total_error_frequency*100:.5f} %")
|
||||
|
||||
# do plus minus
|
||||
print(f"New Error: {total_results['mean_error']:.5f} ± {total_results['confidence_interval'][1] - total_results['mean_error']:.5f} seconds")
|
||||
|
||||
plt.figure(figsize=(10, 5))
|
||||
sns.histplot(all_errors, bins=12, kde=False)
|
||||
plt.title('Distribution of Newly Introduced Errors (macOS)')
|
||||
plt.xlabel('Error Duration (seconds)')
|
||||
plt.ylabel('Frequency')
|
||||
plt.savefig('error_dist', dpi=300)
|
||||
plt.show()
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,39 +0,0 @@
|
||||
import signal
|
||||
import sys
|
||||
import traceback
|
||||
|
||||
from PyQt6.QtWidgets import QApplication
|
||||
|
||||
from ducktrack import MainInterface
|
||||
|
||||
|
||||
def main():
|
||||
app = QApplication(sys.argv)
|
||||
app.setQuitOnLastWindowClosed(False)
|
||||
signal.signal(signal.SIGINT, signal.SIG_DFL)
|
||||
interface = MainInterface(app)
|
||||
interface.show()
|
||||
|
||||
# TODO: come up with a better error solution to this
|
||||
|
||||
original_excepthook = sys.excepthook
|
||||
def handle_exception(exc_type, exc_value, exc_traceback):
|
||||
print("Exception type:", exc_type)
|
||||
print("Exception value:", exc_value)
|
||||
|
||||
trace_details = traceback.format_exception(exc_type, exc_value, exc_traceback)
|
||||
trace_string = "".join(trace_details)
|
||||
|
||||
print("Exception traceback:", trace_string)
|
||||
|
||||
message = f"An error occurred!\n\n{exc_value}\n\n{trace_string}"
|
||||
interface.display_error_message(message)
|
||||
|
||||
original_excepthook(exc_type, exc_value, exc_traceback)
|
||||
|
||||
sys.excepthook = handle_exception
|
||||
|
||||
sys.exit(app.exec())
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
Before Width: | Height: | Size: 1.8 MiB |
|
Before Width: | Height: | Size: 156 KiB |
|
Before Width: | Height: | Size: 176 KiB |
|
Before Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 170 KiB |
|
Before Width: | Height: | Size: 156 KiB |
|
Before Width: | Height: | Size: 127 KiB |
|
Before Width: | Height: | Size: 94 KiB |
|
Before Width: | Height: | Size: 192 KiB |
|
Before Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 185 KiB |
|
Before Width: | Height: | Size: 166 KiB |
|
Before Width: | Height: | Size: 56 KiB |
|
Before Width: | Height: | Size: 68 KiB |
|
Before Width: | Height: | Size: 57 KiB |
|
Before Width: | Height: | Size: 64 KiB |
|
Before Width: | Height: | Size: 442 KiB |
|
Before Width: | Height: | Size: 88 KiB |
|
Before Width: | Height: | Size: 836 KiB |
|
Before Width: | Height: | Size: 674 KiB |
|
Before Width: | Height: | Size: 248 KiB |
|
Before Width: | Height: | Size: 109 KiB |
|
Before Width: | Height: | Size: 144 KiB |
|
Before Width: | Height: | Size: 190 KiB |
|
Before Width: | Height: | Size: 13 KiB |
@@ -1,9 +0,0 @@
|
||||
git+https://github.com/moses-palmer/pynput.git@refs/pull/541/head # to make sure that it works on Apple Silicon
|
||||
pyautogui
|
||||
obsws-python
|
||||
PyQt6
|
||||
Pillow
|
||||
screeninfo
|
||||
wmi
|
||||
psutil
|
||||
pyinstaller
|
||||
@@ -1,12 +1,16 @@
|
||||
# Setup Instructions
|
||||
# Evaluator Setup Details
|
||||
Setup scaffolding for the evaluators in the desktop environment for those who want to know the details of the evaluator setup for customized evaluation and extension
|
||||
|
||||
## Overall
|
||||
Disable the system crash report by:
|
||||
Inside the virtual machine, disable the system crash report by:
|
||||
```
|
||||
sudo vim /etc/default/apport
|
||||
```
|
||||
and then change the `enabled` to `0`.
|
||||
|
||||
## VSCode
|
||||
todo
|
||||
|
||||
## LibreOffice
|
||||
For LibreOffice, please enter into the app first, and then enable the no pop-up when 'ctrl + s'.
|
||||
|
||||
|
||||
@@ -558,39 +558,54 @@ def get_active_url_from_accessTree(env, config):
|
||||
Return
|
||||
url: str
|
||||
"""
|
||||
accessibility_tree: str = env.controller.get_accessibility_tree()
|
||||
# download accessibility tree to "/home/user/Desktop"
|
||||
logger.debug("AT@eval: %s", accessibility_tree)
|
||||
# first, use accessibility API to get the active tab URL
|
||||
at: _Element = lxml.etree.fromstring(accessibility_tree)
|
||||
arch = platform.machine()
|
||||
print("Your architecture is: {}".format(arch))
|
||||
try:
|
||||
if "arm" in arch:
|
||||
selector = CSSSelector("application[name=Chromium] entry[name=Address\\ and\\ search\\ bar]",
|
||||
namespaces=_accessibility_ns_map)
|
||||
else:
|
||||
selector = CSSSelector("application[name=Google\\ Chrome] entry[name=Address\\ and\\ search\\ bar]",
|
||||
namespaces=_accessibility_ns_map)
|
||||
except:
|
||||
logger.error("Failed to parse the selector for active tab URL")
|
||||
return None
|
||||
elements: List[_Element] = selector(at)
|
||||
# if "xpath" in config:
|
||||
# elements: List[_Element] = at.xpath(config["xpath"], namespaces=_accessibility_ns_map)
|
||||
# elif "selectors" in config:
|
||||
# selector = CSSSelector(", ".join(config["selectors"]), namespaces=_accessibility_ns_map)
|
||||
# elements: List[_Element] = selector(at)
|
||||
if len(elements) == 0:
|
||||
print("no elements found")
|
||||
return None
|
||||
elif elements[-1].text is None:
|
||||
print("no text found")
|
||||
# Ensure the controller and its method are accessible and return a valid result
|
||||
if hasattr(env, 'controller') and callable(getattr(env.controller, 'get_accessibility_tree', None)):
|
||||
accessibility_tree = env.controller.get_accessibility_tree()
|
||||
if accessibility_tree is None:
|
||||
print("Failed to get the accessibility tree.")
|
||||
return None
|
||||
else:
|
||||
print("Controller or method 'get_accessibility_tree' not found.")
|
||||
return None
|
||||
|
||||
active_tab_url = config["goto_prefix"] + elements[0].text if "goto_prefix" in config.keys() else "https://" + \
|
||||
elements[0].text
|
||||
print("active tab url now: {}".format(active_tab_url))
|
||||
logger.debug("AT@eval: %s", accessibility_tree)
|
||||
|
||||
at = None
|
||||
try:
|
||||
at = lxml.etree.fromstring(accessibility_tree)
|
||||
except ValueError as e:
|
||||
logger.error(f"Error parsing accessibility tree: {e}")
|
||||
return None
|
||||
|
||||
# Determine the correct selector based on system architecture
|
||||
selector = None
|
||||
arch = platform.machine()
|
||||
print(f"Your architecture is: {arch}")
|
||||
|
||||
if "arm" in arch:
|
||||
selector_string = "application[name=Chromium] entry[name=Address\\ and\\ search\\ bar]"
|
||||
else:
|
||||
selector_string = "application[name=Google\\ Chrome] entry[name=Address\\ and\\ search\\ bar]"
|
||||
|
||||
try:
|
||||
selector = CSSSelector(selector_string, namespaces=_accessibility_ns_map)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to parse the selector for active tab URL: {e}")
|
||||
return None
|
||||
|
||||
elements = selector(at) if selector else []
|
||||
if not elements:
|
||||
print("No elements found.")
|
||||
return None
|
||||
elif not elements[-1].text:
|
||||
print("No text found in the latest element.")
|
||||
return None
|
||||
|
||||
# Use a default prefix if 'goto_prefix' is not specified in the config
|
||||
goto_prefix = config.get("goto_prefix", "https://")
|
||||
|
||||
active_tab_url = f"{goto_prefix}{elements[0].text}"
|
||||
print(f"Active tab url now: {active_tab_url}")
|
||||
return active_tab_url
|
||||
|
||||
|
||||
@@ -1079,18 +1094,18 @@ def get_data_delete_automacally(env, config: Dict[str, str]):
|
||||
def get_active_tab_html_parse(env, config: Dict[str, Any]):
|
||||
"""
|
||||
This function is used to get the specific element's text content from the active tab's html.
|
||||
config:
|
||||
config:
|
||||
Dict[str, str]{
|
||||
# Keys used in get_active_url_from_accessTree: "xpath", "selectors"
|
||||
'category':
|
||||
'category':
|
||||
choose from ["class", "label", "xpath", "input"], used to indicate how to find the element
|
||||
'labelObject':
|
||||
'labelObject':
|
||||
only exists when category is "label",
|
||||
a dict like { "labelSelector": "the key you want to store the text content of this label's ee=lement"}
|
||||
'class_singleObject':
|
||||
only exists when category is "class", a dict with keys as the class name,
|
||||
'class_singleObject':
|
||||
only exists when category is "class", a dict with keys as the class name,
|
||||
like { "class name" : "the key you want to store the text content of this element" }
|
||||
'class_multiObject':
|
||||
'class_multiObject':
|
||||
only exists when category is "class", used for elements with same class name.
|
||||
Two layer of dict, like
|
||||
( {
|
||||
@@ -1099,8 +1114,8 @@ def get_active_tab_html_parse(env, config: Dict[str, Any]):
|
||||
...
|
||||
}
|
||||
} )
|
||||
'xpathObject':
|
||||
only exists when category is "xpath", a dict with keys as the xpath,
|
||||
'xpathObject':
|
||||
only exists when category is "xpath", a dict with keys as the xpath,
|
||||
like { "full xpath" : "the key you want to store the text content of this element" }
|
||||
'inputObject':
|
||||
only exists when category is "input",
|
||||
@@ -1151,32 +1166,50 @@ def get_active_tab_html_parse(env, config: Dict[str, Any]):
|
||||
if target_page is None:
|
||||
logger.error("Your tab is not the target tab.")
|
||||
return {}
|
||||
|
||||
return_json = {}
|
||||
|
||||
def safely_get_text_content(selector):
|
||||
elements = target_page.query_selector_all(selector)
|
||||
return [element.text_content().strip() for element in elements if element]
|
||||
|
||||
if config["category"] == "class":
|
||||
# find the text of elements in html with specific class name
|
||||
class_multiObject = config["class_multiObject"]
|
||||
for key in class_multiObject.keys():
|
||||
object_dict = class_multiObject[key]
|
||||
for order_key in object_dict.keys():
|
||||
return_json[object_dict[order_key]] = target_page.query_selector_all("." + key)[
|
||||
int(order_key)].text_content().strip()
|
||||
class_singleObject = config["class_singleObject"]
|
||||
for key in class_singleObject.keys():
|
||||
return_json[class_singleObject[key]] = target_page.query_selector("." + key).text_content().strip()
|
||||
class_multiObject = config.get("class_multiObject", {})
|
||||
for class_name, object_dict in class_multiObject.items():
|
||||
elements_texts = safely_get_text_content("." + class_name)
|
||||
for order_key, key in object_dict.items():
|
||||
index = int(order_key)
|
||||
if len(elements_texts) > index:
|
||||
return_json[key] = elements_texts[index]
|
||||
|
||||
class_singleObject = config.get("class_singleObject", {})
|
||||
for class_name, key in class_singleObject.items():
|
||||
element_text = safely_get_text_content("." + class_name)
|
||||
if element_text:
|
||||
return_json[key] = element_text[0]
|
||||
|
||||
elif config['category'] == "label":
|
||||
# find the text of elements in html with specific label name
|
||||
labelObject = config["labelObject"]
|
||||
for key in labelObject.keys():
|
||||
return_json[labelObject[key]] = target_page.get_by_label(key).text_content().strip()
|
||||
# Assuming get_by_label is a custom function or part of the framework being used
|
||||
labelObject = config.get("labelObject", {})
|
||||
for labelSelector, key in labelObject.items():
|
||||
text = target_page.locator(f"text={labelSelector}").first.text_content().strip()
|
||||
if text:
|
||||
return_json[key] = text
|
||||
|
||||
elif config["category"] == "xpath":
|
||||
# find the text of elements in html with specific xpath
|
||||
xpathObject = config["xpathObject"]
|
||||
for key in xpathObject.keys():
|
||||
return_json[xpathObject[key]] = target_page.locator("xpath=" + key).text_content().strip()
|
||||
xpathObject = config.get("xpathObject", {})
|
||||
for xpath, key in xpathObject.items():
|
||||
elements = target_page.locator(f"xpath={xpath}")
|
||||
if elements.count() > 0:
|
||||
return_json[key] = elements.first.text_content().strip()
|
||||
|
||||
elif config["category"] == "input":
|
||||
inputObject = config["inputObject"]
|
||||
for key in inputObject.keys():
|
||||
return_json[inputObject[key]] = target_page.locator("xpath=" + key).input_value().strip()
|
||||
inputObjects = config.get("inputObject", {})
|
||||
for xpath, key in inputObjects.items():
|
||||
inputs = target_page.locator(f"xpath={xpath}")
|
||||
if inputs.count() > 0:
|
||||
return_json[key] = inputs.first.input_value().strip()
|
||||
|
||||
browser.close()
|
||||
return return_json
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import zipfile
|
||||
from io import BytesIO
|
||||
from typing import List, Dict, Any
|
||||
|
||||
import easyocr
|
||||
from PIL import Image
|
||||
from docx import Document
|
||||
from docx.enum.text import WD_PARAGRAPH_ALIGNMENT, WD_TAB_ALIGNMENT
|
||||
@@ -247,8 +248,9 @@ def compare_docx_images(docx_file1, docx_file2):
|
||||
return 1
|
||||
|
||||
|
||||
import easyocr
|
||||
def compare_image_text(image_path, rule):
|
||||
if not image_path:
|
||||
return 0
|
||||
reader = easyocr.Reader(['en'])
|
||||
result = reader.readtext(image_path)
|
||||
extracted_text = ' '.join([entry[1] for entry in result])
|
||||
|
||||
@@ -20,7 +20,7 @@ from lxml.cssselect import CSSSelector
|
||||
from lxml.etree import _Element
|
||||
from rapidfuzz import fuzz
|
||||
|
||||
from .utils import _match_record, _match_value_to_rule
|
||||
from desktop_env.evaluators.metrics.utils import _match_record, _match_value_to_rule
|
||||
|
||||
logger = logging.getLogger("desktopenv.metric.general")
|
||||
|
||||
@@ -321,11 +321,11 @@ def check_direct_json_object(result, rules) -> float:
|
||||
expected_json = rules["expected"]
|
||||
|
||||
for key in expected_json.keys():
|
||||
if isinstance(expected_json.get(key), List):
|
||||
if isinstance(expected_json.get(key), list):
|
||||
flag = 0
|
||||
expected_value_list = expected_json.get(key)
|
||||
for each_expected_value in expected_value_list:
|
||||
if each_expected_value in result.get(key):
|
||||
if isinstance(result.get(key), list) and each_expected_value in result.get(key):
|
||||
flag = 1
|
||||
break
|
||||
if flag == 0:
|
||||
@@ -484,3 +484,15 @@ def compare_python_pure_text(py_file_path, gold_file_path):
|
||||
return 1
|
||||
else:
|
||||
return 0
|
||||
|
||||
if __name__ == '__main__':
|
||||
print(check_direct_json_object([], rules={
|
||||
"relativeTime": {
|
||||
"from": "5th next month"
|
||||
},
|
||||
"expected": {
|
||||
"start": "SEA",
|
||||
"end": "NYC",
|
||||
"time": "{DoW}, {Month} {DayD}, {Year}",
|
||||
"category": "Miles"
|
||||
}}))
|
||||
65
mm_agents/README.md
Normal file
@@ -0,0 +1,65 @@
|
||||
# Agent
|
||||
## Prompt-based Agents
|
||||
|
||||
### Supported Models
|
||||
We currently support the following models as the foundation models for the agents:
|
||||
- `GPT-3.5` (gpt-3.5-turbo-16k, ...)
|
||||
- `GPT-4` (gpt-4-0125-preview, gpt-4-1106-preview, ...)
|
||||
- `GPT-4V` (gpt-4-vision-preview, ...)
|
||||
- `Gemini-Pro`
|
||||
- `Gemini-Pro-Vision`
|
||||
- `Claude-3, 2` (claude-3-haiku-2024030, claude-3-sonnet-2024022, ...)
|
||||
- ...
|
||||
|
||||
And those from open-source community:
|
||||
- `Mixtral 8x7B`
|
||||
- `QWEN`, `QWEN-VL`
|
||||
- `CogAgent`
|
||||
- ...
|
||||
|
||||
And we will integrate and support more foundation models to support digital agent in the future, stay tuned.
|
||||
|
||||
### How to use
|
||||
|
||||
```python
|
||||
from mm_agents.agent import PromptAgent
|
||||
|
||||
agent = PromptAgent(
|
||||
model="gpt-4-0125-preview",
|
||||
observation_type="screenshot",
|
||||
)
|
||||
agent.reset()
|
||||
# say we have a instruction and observation
|
||||
instruction = "Please help me to find the nearest restaurant."
|
||||
obs = {"screenshot": "path/to/observation.jpg"}
|
||||
response, actions = agent.predict(
|
||||
instruction,
|
||||
obs
|
||||
)
|
||||
```
|
||||
|
||||
### Observation Space and Action Space
|
||||
We currently support the following observation spaces:
|
||||
- `a11y_tree`: the a11y tree of the current screen
|
||||
- `screenshot`: a screenshot of the current screen
|
||||
- `screenshot_a11y_tree`: a screenshot of the current screen with a11y tree
|
||||
- `som`: the set-of-mark trick on the current screen, with a table metadata
|
||||
|
||||
And the following action spaces:
|
||||
- `pyautogui`: valid python code with `pyauotgui` code valid
|
||||
- `computer_13`: a set of enumerated actions designed by us
|
||||
|
||||
To use feed an observation into the agent, you have to keep the obs variable as a dict with the corresponding information:
|
||||
```python
|
||||
obs = {
|
||||
"screenshot": "path/to/observation.jpg",
|
||||
"a11y_tree": "" # [a11y_tree data]
|
||||
}
|
||||
response, actions = agent.predict(
|
||||
instruction,
|
||||
obs
|
||||
)
|
||||
```
|
||||
|
||||
## Efficient Agents, Q* Agents, and more
|
||||
Stay tuned for more updates.
|
||||
@@ -180,6 +180,7 @@ def trim_accessibility_tree(linearized_accessibility_tree, max_tokens):
|
||||
linearized_accessibility_tree += "[...]\n"
|
||||
return linearized_accessibility_tree
|
||||
|
||||
|
||||
class PromptAgent:
|
||||
def __init__(
|
||||
self,
|
||||
@@ -572,22 +573,10 @@ class PromptAgent:
|
||||
|
||||
logger.debug("CLAUDE MESSAGE: %s", repr(claude_messages))
|
||||
|
||||
# headers = {
|
||||
# "x-api-key": os.environ["ANTHROPIC_API_KEY"],
|
||||
# "anthropic-version": "2023-06-01",
|
||||
# "content-type": "application/json"
|
||||
# }
|
||||
|
||||
# headers = {
|
||||
# "Accept": "application / json",
|
||||
# "Authorization": "Bearer " + os.environ["ANTHROPIC_API_KEY"],
|
||||
# "User-Agent": "Apifox/1.0.0 (https://apifox.com)",
|
||||
# "Content-Type": "application/json"
|
||||
# }
|
||||
|
||||
headers = {
|
||||
"Authorization": os.environ["ANTHROPIC_API_KEY"],
|
||||
"Content-Type": "application/json"
|
||||
"x-api-key": os.environ["ANTHROPIC_API_KEY"],
|
||||
"anthropic-version": "2023-06-01",
|
||||
"content-type": "application/json"
|
||||
}
|
||||
|
||||
payload = {
|
||||
@@ -598,28 +587,21 @@ class PromptAgent:
|
||||
"top_p": top_p
|
||||
}
|
||||
|
||||
max_attempts = 20
|
||||
attempt = 0
|
||||
while attempt < max_attempts:
|
||||
# response = requests.post("https://api.aigcbest.top/v1/chat/completions", headers=headers, json=payload)
|
||||
response = requests.post("https://token.cluade-chat.top/v1/chat/completions", headers=headers,
|
||||
json=payload)
|
||||
if response.status_code == 200:
|
||||
result = response.json()['choices'][0]['message']['content']
|
||||
break
|
||||
else:
|
||||
logger.error(f"Failed to call LLM: {response.text}")
|
||||
time.sleep(10)
|
||||
attempt += 1
|
||||
response = requests.post(
|
||||
"https://api.anthropic.com/v1/messages",
|
||||
headers=headers,
|
||||
json=payload
|
||||
)
|
||||
|
||||
if response.status_code != 200:
|
||||
|
||||
logger.error("Failed to call LLM: " + response.text)
|
||||
time.sleep(5)
|
||||
return ""
|
||||
else:
|
||||
print("Exceeded maximum attempts to call LLM.")
|
||||
result = ""
|
||||
|
||||
return result
|
||||
|
||||
return response.json()['content'][0]['text']
|
||||
|
||||
elif self.model.startswith("mistral"):
|
||||
print("Call mistral")
|
||||
messages = payload["messages"]
|
||||
max_tokens = payload["max_tokens"]
|
||||
top_p = payload["top_p"]
|
||||
@@ -652,7 +634,9 @@ class PromptAgent:
|
||||
response = client.chat.completions.create(
|
||||
messages=mistral_messages,
|
||||
model=self.model,
|
||||
max_tokens=max_tokens
|
||||
max_tokens=max_tokens,
|
||||
top_p=top_p,
|
||||
temperature=temperature
|
||||
)
|
||||
break
|
||||
except:
|
||||
@@ -670,7 +654,6 @@ class PromptAgent:
|
||||
|
||||
elif self.model.startswith("THUDM"):
|
||||
# THUDM/cogagent-chat-hf
|
||||
print("Call CogAgent")
|
||||
messages = payload["messages"]
|
||||
max_tokens = payload["max_tokens"]
|
||||
top_p = payload["top_p"]
|
||||
@@ -703,7 +686,9 @@ class PromptAgent:
|
||||
payload = {
|
||||
"model": self.model,
|
||||
"max_tokens": max_tokens,
|
||||
"messages": cog_messages
|
||||
"messages": cog_messages,
|
||||
"temperature": temperature,
|
||||
"top_p": top_p
|
||||
}
|
||||
|
||||
base_url = "http://127.0.0.1:8000"
|
||||
@@ -717,7 +702,6 @@ class PromptAgent:
|
||||
print("Failed to call LLM: ", response.status_code)
|
||||
return ""
|
||||
|
||||
|
||||
elif self.model.startswith("gemini"):
|
||||
def encoded_img_to_pil_img(data_str):
|
||||
base64_str = data_str.replace("data:image/png;base64,", "")
|
||||
@@ -802,7 +786,8 @@ class PromptAgent:
|
||||
messages = payload["messages"]
|
||||
max_tokens = payload["max_tokens"]
|
||||
top_p = payload["top_p"]
|
||||
temperature = payload["temperature"]
|
||||
if payload["temperature"]:
|
||||
logger.warning("Qwen model does not support temperature parameter, it will be ignored.")
|
||||
|
||||
qwen_messages = []
|
||||
|
||||
@@ -821,7 +806,9 @@ class PromptAgent:
|
||||
|
||||
response = dashscope.MultiModalConversation.call(
|
||||
model='qwen-vl-plus',
|
||||
messages=messages, # todo: add the hyperparameters
|
||||
messages=messages,
|
||||
max_length=max_tokens,
|
||||
top_p=top_p,
|
||||
)
|
||||
# The response status_code is HTTPStatus.OK indicate success,
|
||||
# otherwise indicate request is failed, you can get error code
|
||||
|
||||
|
Before Width: | Height: | Size: 1.5 MiB |
@@ -19,6 +19,28 @@ My computer's password is 'password', feel free to use it when you need sudo rig
|
||||
First give the current screenshot and previous things we did a short reflection, then RETURN ME THE CODE OR SPECIAL CODE I ASKED FOR. NEVER EVER RETURN ME ANYTHING ELSE.
|
||||
""".strip()
|
||||
|
||||
SYS_PROMPT_IN_SCREENSHOT_OUT_CODE_FEW_SHOT = """
|
||||
You are an agent which follow my instruction and perform desktop computer tasks as instructed.
|
||||
You have good knowledge of computer and good internet connection and assume your code will run on a computer for controlling the mouse and keyboard.
|
||||
For each step, you will get an observation of an image, which is the screenshot of the computer screen and the instruction and you will predict the next action to operate on the computer based on the image.
|
||||
|
||||
You are required to use `pyautogui` to perform the action grounded to the observation, but DONOT use the `pyautogui.locateCenterOnScreen` function to locate the element you want to operate with since we have no image of the element you want to operate with. DONOT USE `pyautogui.screenshot()` to make screenshot.
|
||||
Return one line or multiple lines of python code to perform the action each time, be time efficient. When predicting multiple lines of code, make some small sleep like `time.sleep(0.5);` interval so that the machine could take; Each time you need to predict a complete code, no variables or function can be shared from history
|
||||
You need to to specify the coordinates of by yourself based on your observation of current observation, but you should be careful to ensure that the coordinates are correct.
|
||||
You ONLY need to return the code inside a code block, like this:
|
||||
```python
|
||||
# your code here
|
||||
```
|
||||
Specially, it is also allowed to return the following special code:
|
||||
When you think you have to wait for some time, return ```WAIT```;
|
||||
When you think the task can not be done, return ```FAIL```, don't easily say ```FAIL```, try your best to do the task;
|
||||
When you think the task is done, return ```DONE```.
|
||||
|
||||
My computer's password is 'password', feel free to use it when you need sudo rights.
|
||||
Our past communication is great, and what you have done is very helpful. I will now give you another task to complete.
|
||||
First take a deep breath, think step by step, give the current screenshot a thinking, then RETURN ME THE CODE OR SPECIAL CODE I ASKED FOR. NEVER EVER RETURN ME ANYTHING ELSE.
|
||||
""".strip()
|
||||
|
||||
SYS_PROMPT_IN_SCREENSHOT_OUT_ACTION = """
|
||||
You will act as an agent which follow my instruction and perform desktop computer tasks as instructed. You must have good knowledge of computer and good internet connection.
|
||||
For each step, you will get an observation of an image, which is the screenshot of the computer screen. And you will predict the action of the computer based on the image.
|
||||
@@ -264,6 +286,253 @@ You MUST choose and ONLY CHOOSE from the action space above, otherwise your acti
|
||||
You CAN predict multiple actions at one step, but you should only return one action for each step.
|
||||
""".strip()
|
||||
|
||||
SYS_PROMPT_IN_SCREENSHOT_OUT_ACTION_FEW_SHOT = """
|
||||
You will act as an agent which follow my instruction and perform desktop computer tasks as instructed. You must have good knowledge of computer and good internet connection.
|
||||
For each step, you will get an observation of an image, which is the screenshot of the computer screen and a task instruction. And you will predict the action of the computer based on the image.
|
||||
|
||||
HERE is the description of the action space you need to predict, follow the format and choose the correct action type and parameters:
|
||||
ACTION_SPACE = [
|
||||
{
|
||||
"action_type": "MOVE_TO",
|
||||
"note": "move the cursor to the specified position",
|
||||
"parameters": {
|
||||
"x": {
|
||||
"type": float,
|
||||
"range": [0, X_MAX],
|
||||
"optional": False,
|
||||
},
|
||||
"y": {
|
||||
"type": float,
|
||||
"range": [0, Y_MAX],
|
||||
"optional": False,
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"action_type": "CLICK",
|
||||
"note": "click the left button if the button not specified, otherwise click the specified button; click at the current position if x and y are not specified, otherwise click at the specified position",
|
||||
"parameters": {
|
||||
"button": {
|
||||
"type": str,
|
||||
"range": ["left", "right", "middle"],
|
||||
"optional": True,
|
||||
},
|
||||
"x": {
|
||||
"type": float,
|
||||
"range": [0, X_MAX],
|
||||
"optional": True,
|
||||
},
|
||||
"y": {
|
||||
"type": float,
|
||||
"range": [0, Y_MAX],
|
||||
"optional": True,
|
||||
},
|
||||
"num_clicks": {
|
||||
"type": int,
|
||||
"range": [1, 2, 3],
|
||||
"optional": True,
|
||||
},
|
||||
}
|
||||
},
|
||||
{
|
||||
"action_type": "MOUSE_DOWN",
|
||||
"note": "press the left button if the button not specified, otherwise press the specified button",
|
||||
"parameters": {
|
||||
"button": {
|
||||
"type": str,
|
||||
"range": ["left", "right", "middle"],
|
||||
"optional": True,
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"action_type": "MOUSE_UP",
|
||||
"note": "release the left button if the button not specified, otherwise release the specified button",
|
||||
"parameters": {
|
||||
"button": {
|
||||
"type": str,
|
||||
"range": ["left", "right", "middle"],
|
||||
"optional": True,
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"action_type": "RIGHT_CLICK",
|
||||
"note": "right click at the current position if x and y are not specified, otherwise right click at the specified position",
|
||||
"parameters": {
|
||||
"x": {
|
||||
"type": float,
|
||||
"range": [0, X_MAX],
|
||||
"optional": True,
|
||||
},
|
||||
"y": {
|
||||
"type": float,
|
||||
"range": [0, Y_MAX],
|
||||
"optional": True,
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"action_type": "DOUBLE_CLICK",
|
||||
"note": "double click at the current position if x and y are not specified, otherwise double click at the specified position",
|
||||
"parameters": {
|
||||
"x": {
|
||||
"type": float,
|
||||
"range": [0, X_MAX],
|
||||
"optional": True,
|
||||
},
|
||||
"y": {
|
||||
"type": float,
|
||||
"range": [0, Y_MAX],
|
||||
"optional": True,
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"action_type": "DRAG_TO",
|
||||
"note": "drag the cursor to the specified position with the left button pressed",
|
||||
"parameters": {
|
||||
"x": {
|
||||
"type": float,
|
||||
"range": [0, X_MAX],
|
||||
"optional": False,
|
||||
},
|
||||
"y": {
|
||||
"type": float,
|
||||
"range": [0, Y_MAX],
|
||||
"optional": False,
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"action_type": "SCROLL",
|
||||
"note": "scroll the mouse wheel up or down",
|
||||
"parameters": {
|
||||
"dx": {
|
||||
"type": int,
|
||||
"range": None,
|
||||
"optional": False,
|
||||
},
|
||||
"dy": {
|
||||
"type": int,
|
||||
"range": None,
|
||||
"optional": False,
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"action_type": "TYPING",
|
||||
"note": "type the specified text",
|
||||
"parameters": {
|
||||
"text": {
|
||||
"type": str,
|
||||
"range": None,
|
||||
"optional": False,
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"action_type": "PRESS",
|
||||
"note": "press the specified key and release it",
|
||||
"parameters": {
|
||||
"key": {
|
||||
"type": str,
|
||||
"range": KEYBOARD_KEYS,
|
||||
"optional": False,
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"action_type": "KEY_DOWN",
|
||||
"note": "press the specified key",
|
||||
"parameters": {
|
||||
"key": {
|
||||
"type": str,
|
||||
"range": KEYBOARD_KEYS,
|
||||
"optional": False,
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"action_type": "KEY_UP",
|
||||
"note": "release the specified key",
|
||||
"parameters": {
|
||||
"key": {
|
||||
"type": str,
|
||||
"range": KEYBOARD_KEYS,
|
||||
"optional": False,
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"action_type": "HOTKEY",
|
||||
"note": "press the specified key combination",
|
||||
"parameters": {
|
||||
"keys": {
|
||||
"type": list,
|
||||
"range": [KEYBOARD_KEYS],
|
||||
"optional": False,
|
||||
}
|
||||
}
|
||||
},
|
||||
############################################################################################################
|
||||
{
|
||||
"action_type": "WAIT",
|
||||
"note": "wait until the next action",
|
||||
},
|
||||
{
|
||||
"action_type": "FAIL",
|
||||
"note": "decide the task can not be performed",
|
||||
},
|
||||
{
|
||||
"action_type": "DONE",
|
||||
"note": "decide the task is done",
|
||||
}
|
||||
]
|
||||
Firstly you need to predict the class of your action, then you need to predict the parameters of your action:
|
||||
- For MOUSE_MOVE, you need to predict the x and y coordinate of the mouse cursor, the left top corner of the screen is (0, 0), the right bottom corner of the screen is (1920, 1080)
|
||||
for example, format as:
|
||||
```
|
||||
{
|
||||
"action_type": "MOUSE_MOVE",
|
||||
"x": 1319.11,
|
||||
"y": 65.06
|
||||
}
|
||||
```
|
||||
- For [CLICK, MOUSE_DOWN, MOUSE_UP], you need to specify the click_type as well, select from [LEFT, MIDDLE, RIGHT, WHEEL_UP, WHEEL_DOWN], which means you click the left button, middle button, right button, wheel up or wheel down of your mouse:
|
||||
for example, format as:
|
||||
```
|
||||
{
|
||||
"action_type": "CLICK",
|
||||
"click_type": "LEFT"
|
||||
}
|
||||
```
|
||||
- For [KEY, KEY_DOWN, KEY_UP], you need to choose a(multiple) key(s) from the keyboard
|
||||
for example, format as:
|
||||
```
|
||||
{
|
||||
"action_type": "KEY",
|
||||
"key": "ctrl+c"
|
||||
}
|
||||
```
|
||||
- For TYPE, you need to specify the text you want to type
|
||||
for example, format as:
|
||||
```
|
||||
{
|
||||
"action_type": "TYPE",
|
||||
"text": "hello world"
|
||||
}
|
||||
```
|
||||
|
||||
REMEMBER:
|
||||
For every step, you should only RETURN ME THE action_type AND parameters I ASKED FOR. NEVER EVER RETURN ME ANYTHING ELSE.
|
||||
You MUST wrap the dict with backticks (\`).
|
||||
You MUST choose and ONLY CHOOSE from the action space above, otherwise your action will be considered as invalid and you will get a penalty.
|
||||
You CAN predict multiple actions at one step, but you should only return one action for each step.
|
||||
Our past communication is great, and what you have done is very helpful. I will now give you another task to complete.
|
||||
""".strip()
|
||||
|
||||
|
||||
SYS_PROMPT_IN_A11Y_OUT_CODE = """
|
||||
You are an agent which follow my instruction and perform desktop computer tasks as instructed.
|
||||
You have good knowledge of computer and good internet connection and assume your code will run on a computer for controlling the mouse and keyboard.
|
||||
|
||||
@@ -50,3 +50,4 @@ pypdf2
|
||||
pdfplumber
|
||||
wandb
|
||||
wrapt_timeout_decorator
|
||||
gdown
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
# Resource Collection
|
||||
|
||||
Manually gain some insights, then scale with careful code.
|
||||
@@ -1,28 +0,0 @@
|
||||
import praw
|
||||
|
||||
def search_reddit(keyword, subreddit, client_id, client_secret, user_agent, limit=10000):
|
||||
# Initialize PRAW with your Reddit application credentials
|
||||
reddit = praw.Reddit(client_id=client_id,
|
||||
client_secret=client_secret,
|
||||
user_agent=user_agent)
|
||||
|
||||
# Search the specified subreddit for the keyword
|
||||
results = reddit.subreddit(subreddit).search(keyword, limit=limit)
|
||||
|
||||
for post in results:
|
||||
print(f"Title: {post.title}")
|
||||
print(f"URL: {post.url}")
|
||||
print(f"Score: {post.score}")
|
||||
print(f"Comments: {post.num_comments}")
|
||||
print("------------------------")
|
||||
|
||||
# Example usage
|
||||
if __name__ == "__main__":
|
||||
CLIENT_ID = 'YOUR_CLIENT_ID'
|
||||
CLIENT_SECRET = 'YOUR_CLIENT_SECRET'
|
||||
USER_AGENT = 'my_reddit_scraper'
|
||||
|
||||
keyword = "how to"
|
||||
subreddit = "vscode"
|
||||
|
||||
search_reddit(keyword, subreddit, CLIENT_ID, CLIENT_SECRET, USER_AGENT)
|
||||
@@ -1,268 +0,0 @@
|
||||
id,Source,InvolvedApp
|
||||
94d95f96-9699-4208-98ba-3c3119edf9c2,https://help.ubuntu.com/lts/ubuntu-help/addremove-install.html.en,OS
|
||||
bedcedc4-4d72-425e-ad62-21960b11fe0d,https://www.youtube.com/watch?v=D4WyNjt_hbQ&t=2s,OS
|
||||
43c2d64c-bab5-4dcb-a30c-b888321c319a,https://ubuntu.com/tutorials/command-line-for-beginners#4-creating-folders-and-files,OS
|
||||
7688b85f-87a4-4e4a-b2f8-f3d6c3f29b82,https://ubuntu.com/tutorials/command-line-for-beginners#5-moving-and-manipulating-files,OS
|
||||
ec4e3f68-9ea4-4c18-a5c9-69f89d1178b3,https://www.youtube.com/watch?v=D4WyNjt_hbQ&t=2s,OS
|
||||
a462a795-fdc7-4b23-b689-e8b6df786b78,https://help.ubuntu.com/lts/ubuntu-help/shell-exit.html.en,OS
|
||||
f9be0997-4b7c-45c5-b05c-4612b44a6118,https://help.ubuntu.com/lts/ubuntu-help/shell-notifications.html.en,OS
|
||||
ae039631-2b12-4637-84f6-c67d51511be3,https://help.ubuntu.com/lts/ubuntu-help/net-default-browser.html.en,OS
|
||||
e2eb4bf1-aa93-4192-b55d-03e2fb6dfd15,https://help.ubuntu.com/lts/ubuntu-help/contacts-add-remove.html.en,OS
|
||||
28cc3b7e-b194-4bc9-8353-d04c0f4d56d2,https://help.ubuntu.com/lts/ubuntu-help/sound-volume.html.en,OS
|
||||
5ea617a3-0e86-4ba6-aab2-dac9aa2e8d57,https://help.ubuntu.com/lts/ubuntu-help/files-recover.html.en,OS
|
||||
e0df059f-28a6-4169-924f-b9623e7184cc,https://help.ubuntu.com/lts/ubuntu-help/files-rename.html.en,OS
|
||||
ddc75b62-7311-4af8-bfb3-859558542b36,https://help.ubuntu.com/lts/ubuntu-help/addremove-remove.html.en,OS
|
||||
5c433d22-ed9a-4e31-91f5-54cf3e8acd63,https://help.ubuntu.com/lts/ubuntu-help/session-language.html.zh-CN,OS
|
||||
b6781586-6346-41cd-935a-a6b1487918fc,https://help.ubuntu.com/lts/ubuntu-help/clock-timezone.html.en,OS
|
||||
b3d4a89c-53f2-4d6b-8b6a-541fb5d205fa,https://help.ubuntu.com/lts/ubuntu-help/bluetooth-turn-on-off.html.en,OS
|
||||
3ce045a0-877b-42aa-8d2c-b4a863336ab8,https://help.ubuntu.com/lts/ubuntu-help/a11y-font-size.html.en,OS
|
||||
fe41f596-a71b-4c2f-9b2f-9dcd40b568c3,https://help.ubuntu.com/lts/ubuntu-help/power-percentage.html.en,OS
|
||||
a4d98375-215b-4a4d-aee9-3d4370fccc41,https://help.ubuntu.com/lts/ubuntu-help/privacy-screen-lock.html.en,OS
|
||||
765d2b74-88a7-4d50-bf51-34e4106fd24a,https://help.ubuntu.com/lts/ubuntu-help/files-delete.html.en,OS
|
||||
cc9d4f34-1ca0-4a1b-8ff2-09302696acb9,https://superuser.com/questions/178587/how-do-i-detach-a-process-from-terminal-entirely,OS
|
||||
5812b315-e7bd-4265-b51f-863c02174c28,https://superuser.com/questions/149404/create-an-ssh-user-who-only-has-permission-to-access-specific-folders,OS
|
||||
c56de254-a3ec-414e-81a6-83d2ce8c41fa,https://superuser.com/questions/28426/how-to-extract-text-with-ocr-from-a-pdf-on-linux,OS
|
||||
6ebbfb01-ea72-4226-a2a6-dc428e111ed2,https://superuser.com/questions/46748/how-do-i-make-bash-my-default-shell-on-ubuntu,OS
|
||||
4d2b519e-e872-4100-8ea3-fe71ab0f9133,https://stackoverflow.com/questions/11530090/adding-a-new-entry-to-the-path-variable-in-zsh,OS
|
||||
c288e301-e626-4b98-a1ab-159dcb162af5,https://stackoverflow.com/questions/41986507/unable-to-set-default-python-version-to-python3-in-ubuntu,OS
|
||||
13584542-872b-42d8-b299-866967b5c3ef,https://superuser.com/questions/72176/linux-set-default-terminal-size-and-screen-position,OS
|
||||
23393935-50c7-4a86-aeea-2b78fd089c5c,https://superuser.com/questions/91307/copying-only-jpg-from-a-directory-structure-to-another-location-linux,OS
|
||||
f10b16e1-c160-4cb3-989f-7b2ec89bc073,https://www.wikihow.com/Install-Gnome-on-Ubuntu,OS
|
||||
eb03d19a-b88d-4de4-8a64-ca0ac66f426b,https://www.youtube.com/shorts/t9JLUaT55UQ,MS Excel
|
||||
0bf05a7d-b28b-44d2-955a-50b41e24012a,https://www.youtube.com/shorts/FPAQaDTS8VY,MS Excel
|
||||
7b802dad-6e0f-4204-9815-d4e3f57627d8,https://www.youtube.com/shorts/Of-lzeP1usE,MS Excel
|
||||
7a4e4bc8-922c-4c84-865c-25ba34136be1,https://www.youtube.com/shorts/bvUhr1AHs44,MS Excel
|
||||
2bd59342-0664-4ccb-ba87-79379096cc08,https://www.youtube.com/shorts/L3Z-F1QTQFY,MS Excel
|
||||
a9f325aa-8c05-4e4f-8341-9e4358565f4f,https://www.youtube.com/shorts/A0gmEBRKXWs,MS Excel
|
||||
ecb0df7a-4e8d-4a03-b162-053391d3afaf,https://www.youtube.com/shorts/tXOovKn0H68,MS Excel
|
||||
7efeb4b1-3d19-4762-b163-63328d66303b,https://www.youtube.com/shorts/4jzXfZNhfmk,MS Excel
|
||||
4e6fcf72-daf3-439f-a232-c434ce416af6,https://www.youtube.com/shorts/0uxJccNCKcE,MS Excel
|
||||
6054afcb-5bab-4702-90a0-b259b5d3217c,https://www.youtube.com/shorts/JTbZ8sRxkdU,MS Excel
|
||||
abed40dc-063f-4598-8ba5-9fe749c0615d,https://www.youtube.com/shorts/xgf4ZpsEx5M,MS Excel
|
||||
01b269ae-2111-4a07-81fd-3fcd711993b0,https://www.youtube.com/shorts/VrUzPTIwQ04,MS Excel
|
||||
8b1ce5f2-59d2-4dcc-b0b0-666a714b9a14,https://www.youtube.com/shorts/Hbcwu6IQ1ns,MS Excel
|
||||
af2b02f7-acee-4be4-8b66-499fab394915,https://www.youtube.com/shorts/AwKsb5VmtBI,MS Excel
|
||||
da1d63b8-fa12-417b-ba18-f748e5f770f3,https://www.youtube.com/shorts/hquscnbz2-U,MS Excel
|
||||
636380ea-d5f6-4474-b6ca-b2ed578a20f1,https://www.youtube.com/shorts/_BYL6VOHLGw,"MS Excel, Edge"
|
||||
5ba77536-05c5-4aae-a9ff-6e298d094c3e,https://www.youtube.com/shorts/CuBC1evUS5I,MS Excel
|
||||
4bc4eaf4-ca5e-4db2-8138-8d4e65af7c0b,https://www.youtube.com/shorts/1adQWfjN-tI,MS Excel
|
||||
672a1b02-c62f-4ae2-acf0-37f5fb3052b0,https://www.youtube.com/shorts/2rhdQXI4Lng,MS Excel
|
||||
648fe544-16ba-44af-a587-12ccbe280ea6,https://www.youtube.com/shorts/sOPBMWaC6Uc,MS Excel
|
||||
8985d1e4-5b99-4711-add4-88949ebb2308,https://www.youtube.com/shorts/J5ts2Acv9Pc,MS Excel
|
||||
9e606842-2e27-43bf-b1d1-b43289c9589b,https://www.youtube.com/shorts/B-mGYDFOyUs,MS Excel
|
||||
fcb6e45b-25c4-4087-9483-03d714f473a9,https://www.youtube.com/shorts/GZipp7nOZS0,MS Excel
|
||||
68c0c5b7-96f3-4e87-92a7-6c1b967fd2d2,https://www.youtube.com/shorts/JEH5TsK-cCk,"MS Excel, Edge"
|
||||
fff629ea-046e-4793-8eec-1a5a15c3eb35,https://www.youtube.com/shorts/8WybtCdUT6w,MS Excel
|
||||
5c9a206c-bb00-4fb6-bb46-ee675c187df5,https://www.youtube.com/shorts/VbQtMNnq9i4,MS Excel
|
||||
e975ae74-79bd-4672-8d1c-dc841a85781d,https://www.youtube.com/shorts/GjT7gGe5Sr8,MS Excel
|
||||
34a6938a-58da-4897-8639-9b90d6db5391,https://www.youtube.com/shorts/gW37x2TkzOY,MS Excel
|
||||
b5a22759-b4eb-4bf2-aeed-ad14e8615f19,https://www.youtube.com/shorts/3xLa-D0C7Ic,MS Excel
|
||||
2f9913a1-51ed-4db6-bfe0-7e1c95b3139e,https://www.youtube.com/shorts/dGLRcmfVO6Q,MS Excel
|
||||
2558031e-401d-4579-8e00-3ecf540fb492,https://www.mrexcel.com/board/threads/sales-for-the-first-6-weeks.1249213/,MS Excel
|
||||
39aa4e37-dc91-482e-99af-132a612d40f3,https://www.libreofficehelp.com/add-insert-delete-copy-move-rename-a-worksheet-in-libreoffice-calc/,LibreOffice Calc
|
||||
0cecd4f3-74de-457b-ba94-29ad6b5dafb6,https://www.libreofficehelp.com/add-insert-delete-copy-move-rename-a-worksheet-in-libreoffice-calc/,LibreOffice Calc
|
||||
4188d3a4-077d-46b7-9c86-23e1a036f6c1,https://www.libreofficehelp.com/freeze-unfreeze-rows-columns-ranges-calc/,LibreOffice Calc
|
||||
51b11269-2ca8-4b2a-9163-f21758420e78,https://www.reddit.com/r/LibreOfficeCalc/comments/186pcc6/how_to_arrange_numbers_in_a_column_from_minimum/,LibreOffice Calc
|
||||
7e429b8d-a3f0-4ed0-9b58-08957d00b127,https://medium.com/@divyangichaudhari17/how-to-use-vlookup-and-hlookup-in-libre-calc-3370698bb3ff,LibreOffice Calc
|
||||
f5a90742-3fa2-40fc-a564-f29b054e0337,https://superuser.com/questions/1236149/libreoffice-calc-how-to-apply-functions-to-columns,LibreOffice Calc
|
||||
22df9241-f8d7-4509-b7f1-37e501a823f7,https://superuser.com/questions/1767185/how-do-you-move-cells-in-libreoffice-calc,LibreOffice Calc
|
||||
1434ca3e-f9e3-4db8-9ca7-b4c653be7d17,https://www.wikihow.com/Remove-Duplicates-in-Open-Office-Calc,LibreOffice Calc
|
||||
347ef137-7eeb-4c80-a3bb-0951f26a8aff,https://www.youtube.com/watch?v=bgO40-CjYNY,LibreOffice Calc
|
||||
6e99a1ad-07d2-4b66-a1ce-ece6d99c20a5,https://www.youtube.com/watch?v=nl-nXjJurhQ,LibreOffice Calc
|
||||
3aaa4e37-dc91-482e-99af-132a612d40f3,https://www.quora.com/How-can-you-import-export-CSV-files-with-LibreOffice-Calc-or-OpenOffice,LibreOffice Calc
|
||||
0decd4f3-74de-457b-ba94-29ad6b5dafb6,https://justclickhere.co.uk/resources/checkboxes-tick-boxes-libreoffice-calc/,LibreOffice Calc
|
||||
37608790-6147-45d0-9f20-1137bb35703d,https://www.youtube.com/shorts/uzPo_CPCHH8,MS Excel
|
||||
f9584479-3d0d-4c79-affa-9ad7afdd8850,https://youtube.com/shorts/feldd-Pn48c?si=9xJiem2uAHm6Jshb,LibreOffice Calc
|
||||
d681960f-7bc3-4286-9913-a8812ba3261a,https://www.youtube.com/shorts/d7U1S_IsTVM,LibreOffice Calc
|
||||
f6a90742-3fa2-40fc-a564-f29b054e0337,https://www.excel-easy.com/examples/drop-down-list.html,LibreOffice Calc
|
||||
21df9241-f8d7-4509-b7f1-37e501a823f7,https://www.youtube.com/watch?v=p5C4V_AO1UU,LibreOffice Calc
|
||||
1334ca3e-f9e3-4db8-9ca7-b4c653be7d17,https://techcommunity.microsoft.com/t5/excel/excel-workbook-top-way-too-big-can-t-see-rows-and-columns/m-p/4014694,LibreOffice Calc
|
||||
357ef137-7eeb-4c80-a3bb-0951f26a8aff,https://www.reddit.com/r/excel/comments/17zny8u/calculating_total_amount_earned_from_total_hours/,LibreOffice Calc
|
||||
6f99a1ad-07d2-4b66-a1ce-ece6d99c20a5,https://techcommunity.microsoft.com/t5/excel/sumarize-the-sheetnames/m-p/4014716,LibreOffice Calc
|
||||
aa3a8974-2e85-438b-b29e-a64df44deb4b,https://www.quora.com/Libre-Office-Calc-How-do-I-resize-all-cells-in-a-sheet-to-make-them-fit-to-1-page-for-printing-and-exporting-as-PDF,LibreOffice Calc
|
||||
a01fbce3-2793-461f-ab86-43680ccbae25,https://superuser.com/questions/1250677/how-to-set-decimal-separator-in-libre-office-calc,LibreOffice Calc
|
||||
4f07fbe9-70de-4927-a4d5-bb28bc12c52c,https://superuser.com/questions/1081048/libreoffice-calc-how-to-pad-number-to-fixed-decimals-when-used-within-formula,LibreOffice Calc
|
||||
e3b1d5fa-ed00-4129-bda1-1452bd2b6772,https://www.reddit.com/r/libreoffice/comments/tel112/calc_how_to_calculate_sum_by_categories/,LibreOffice Calc
|
||||
ca6a9524-f8e9-4d2f-9364-ab0cad567739,https://www.reddit.com/r/libreoffice/comments/113gmyc/how_to_remove_certain_text_from_cells_in_calc/,LibreOffice Calc
|
||||
a455e8d0-930f-40d2-9575-5e8d2d222f58,https://superuser.com/questions/562944/quickly-fill-blank-cells-in-a-list-in-libreoffice-calc,LibreOffice Calc
|
||||
83ee22c6-7737-49ce-9b5a-138c3e92af04,https://superuser.com/questions/661102/currency-conversion-in-libreoffice-calc,LibreOffice Calc
|
||||
819f61c2-ec77-4d3f-9996-0838ae5aacc8,https://superuser.com/questions/381696/creating-a-column-of-working-days-in-libreoffice-calc,LibreOffice Calc
|
||||
69d577b3-004e-4bca-89b2-0d7c2f6049e3,https://superuser.com/questions/387106/libreoffice-calc-how-to-get-total-for-hhmmss-cells,LibreOffice Calc
|
||||
0a1bf4ca-d4ea-4618-baa5-6e8dc1b46d82,https://superuser.com/questions/571915/sum-up-to-n-highest-value-out-of-a-series,LibreOffice Calc
|
||||
ac9bb6cb-1888-43ab-81e4-a98a547918cd,https://superuser.com/questions/1674211/how-to-change-colour-of-slide-number-in-libre-office,LibreOffice Impress
|
||||
5d901039-a89c-4bfb-967b-bf66f4df075e,https://superuser.com/questions/986776/how-can-i-stretch-an-image-in-a-libreoffice-impress-presentation-to-fill-the-pag,LibreOffice Impress
|
||||
071d4ace-091a-4ec3-886e-f4be55ae375d,https://superuser.com/questions/706860/hide-slide-numbers-and-slide-footer-on-first-and-second-slide-in-libreoffice-imp?rq=1,LibreOffice Impress
|
||||
550ce7e7-747b-495f-b122-acdc4d0b8e54,"https://technical-tips.com/blog/software/text-in-libreoffice-strikethrough--6948#:~:text=To%20strikethrough%20Text%20in%20LibreOffice%201%20In%20your,effect%22%20can%20your%20additionally%2C%20for%20example%2C%20double%20underline.",LibreOffice Impress
|
||||
455d3c66-7dc6-4537-a39a-36d3e9119df7,"https://www.libreofficehelp.com/export-libreoffice-impress-slides-images/#:~:text=Exporting%20a%20single%20slide%20as.jpg%2C.png%2C%20etc%20image%20is,on%20the%20checkbox%20Selection.%20Provide%20jpg%20quality%20options.",LibreOffice Impress
|
||||
af23762e-2bfd-4a1d-aada-20fa8de9ce07,https://superuser.com/questions/1059080/how-to-make-a-summary-slide-in-impress-listing-the-titles-of-all-slides-autom,LibreOffice Impress
|
||||
c59742c0-4323-4b9d-8a02-723c251deaa0,https://www.reddit.com/r/libreoffice/comments/17lcdrp/audio_not_supported_in_libreoffice_impress/,LibreOffice Impress
|
||||
39478d4a-1049-456f-aa77-407811393add,https://www.reddit.com/r/libreoffice/comments/jul3o8/putting_cap_or_hat_or_carat_symbol_in_libre/,LibreOffice Impress
|
||||
c3ad4442-499f-4e58-bc4e-1a1417ea9b8c,http://maharajacollege.ac.in/material/Libreofficeimpresspdf.pdf,LibreOffice Impress
|
||||
ef9d12bd-bcee-4ba0-a40e-918400f43ddf,https://www.reddit.com/r/libreoffice/comments/18elh3y/i_closed_the_slide_pannel_on_the_left_and_idk_how/,LibreOffice Impress
|
||||
9ec204e4-f0a3-42f8-8458-b772a6797cab,https://www.tiktok.com/@lil.d1rt_/video/7247574148887629083,LibreOffice Impress
|
||||
0f84bef9-9790-432e-92b7-eece357603fb,https://stackoverflow.com/questions/29036788/how-to-disable-libreoffice-impress-to-use-multiple-display,LibreOffice Impress
|
||||
ce88f674-ab7a-43da-9201-468d38539e4a,https://justclickhere.co.uk/resources/change-slides-in-impress-to-portrait/,LibreOffice Impress
|
||||
f0a334af-f91b-4c03-b578-aac9bec2b543,https://www.libreofficehelp.com/insert-video-impress-presentation/#Inserting_a_Video_in_Impress,LibreOffice Impress
|
||||
3b27600c-3668-4abd-8f84-7bcdebbccbdb,https://www.libreofficehelp.com/change-slide-background-impress/#All_Slides,LibreOffice Impress
|
||||
a097acff-6266-4291-9fbd-137af7ecd439,https://www.youtube.com/watch?v=DDmEvjs4iBw,LibreOffice Impress
|
||||
21760ecb-8f62-40d2-8d85-0cee5725cb72,https://www.libreofficehelp.com/add-animations-transitions-libreoffice-impress-slides/,LibreOffice Impress
|
||||
3cc4f35d-fa2e-4555-afb9-741b7c062a74,https://documentation.libreoffice.org/assets/Uploads/Documentation/en/IG7.6/IG76-ImpressGuide.pdf,LibreOffice Impress
|
||||
6ada715d-3aae-4a32-a6a7-429b2e43fb93,https://www.quora.com/How-do-you-insert-images-into-a-LibreOffice-Writer-document,LibreOffice Writer
|
||||
ecc2413d-8a48-416e-a3a2-d30106ca36cb,https://www.quora.com/How-can-I-insert-a-blank-page-on-libreoffice,LibreOffice Writer
|
||||
0e47de2a-32e0-456c-a366-8c607ef7a9d2,https://ask.libreoffice.org/t/how-to-start-page-numbering-on-a-certain-page/39931/4,LibreOffice Writer
|
||||
4bcb1253-a636-4df4-8cb0-a35c04dfef31,https://www.libreofficehelp.com/save-export-writer-documents-in-pdf-epub-format/,LibreOffice Writer
|
||||
0810415c-bde4-4443-9047-d5f70165a697,https://www.youtube.com/watch?v=Q_AaL6ljudU,LibreOffice Writer
|
||||
e528b65e-1107-4b8c-8988-490e4fece599,https://www.youtube.com/watch?v=l25Evu4ohKg,LibreOffice Writer
|
||||
66399b0d-8fda-4618-95c4-bfc6191617e9,https://www.youtube.com/watch?v=l25Evu4ohKg,LibreOffice Writer
|
||||
936321ce-5236-426a-9a20-e0e3c5dc536f,https://www.youtube.com/watch?v=l25Evu4ohKg,LibreOffice Writer
|
||||
663876c7-3471-43db-ba51-f410b13d9d7d,https://askubuntu.com/questions/319593/how-to-type-science-equations-in-libre-office,LibreOffice Writer
|
||||
3ef2b351-8a84-4ff2-8724-d86eae9b842e,https://askubuntu.com/questions/1066351/how-do-you-center-align-in-libreoffice#:~:text=Ctrl%20%2B%20e%20will%20Center%20align%20the%20cursor%20for%20you.,LibreOffice Writer
|
||||
45d61a06-6545-4422-97b7-bc76cfa964c1,https://stackoverflow.com/questions/71685737/how-to-replace-all-newlines-with-paragraph-marks-in-libreoffice-write,LibreOffice Writer
|
||||
0b17a146-2934-46c7-8727-73ff6b6483e8,https://askubuntu.com/questions/245695/how-do-you-insert-subscripts-and-superscripts-into-ordinary-non-formula-text-i,LibreOffice Writer
|
||||
0e763496-b6bb-4508-a427-fad0b6c3e195,https://ask.libreoffice.org/t/how-do-i-change-the-font-for-the-whole-document-in-writer/9220,LibreOffice Writer
|
||||
f178a4a9-d090-4b56-bc4c-4b72a61a035d,https://ask.libreoffice.org/t/how-do-i-make-times-new-roman-the-default-font-in-lo/64604,LibreOffice Writer
|
||||
0a0faba3-5580-44df-965d-f562a99b291c,https://stackoverflow.com/questions/64528055/how-to-make-part-of-my-sentence-left-aligned-and-rest-as-right-aligned,LibreOffice Writer
|
||||
e246f6d8-78d7-44ac-b668-fcf47946cb50,https://ask.libreoffice.org/t/how-to-change-text-size-color-of-italic-font/77712,LibreOffice Writer
|
||||
8472fece-c7dd-4241-8d65-9b3cd1a0b568,https://stackoverflow.com/questions/37259827/libreoffice-writer-how-to-set-different-colors-to-each-letter,LibreOffice Writer
|
||||
88fe4b2d-3040-4c70-9a70-546a47764b48,https://stackoverflow.com/questions/56554555/libreoffice-writer-how-to-create-empty-line-space-after-every-period-in-a-par,LibreOffice Writer
|
||||
6a33f9b9-0a56-4844-9c3f-96ec3ffb3ba2,https://superuser.com/questions/762500/how-do-i-find-all-highlighted-text-in-libreoffice-writer,LibreOffice Writer
|
||||
d53ff5ee-3b1a-431e-b2be-30ed2673079b,https://ask.libreoffice.org/t/how-to-convert-all-uppercase-to-lowercase/53341,LibreOffice Writer
|
||||
72b810ef-4156-4d09-8f08-a0cf57e7cefe,https://superuser.com/questions/657792/libreoffice-writer-how-to-apply-strikethrough-text-formatting?rq=1,LibreOffice Writer
|
||||
6f81754e-285d-4ce0-b59e-af7edb02d108,https://superuser.com/questions/789473/remove-duplicate-lines-in-libreoffice-openoffice-writer,LibreOffice Writer
|
||||
41c621f7-3544-49e1-af8d-dafd0f834f75,https://superuser.com/questions/1668018/how-to-auto-format-lines-in-libre-office-writer,LibreOffice Writer
|
||||
b21acd93-60fd-4127-8a43-2f5178f4a830,https://superuser.com/questions/1097199/how-can-i-double-space-a-document-in-libreoffice?rq=1,LibreOffice Writer
|
||||
59f21cfb-0120-4326-b255-a5b827b38967,https://docs.videolan.me/vlc-user/desktop/3.0/en/basic/media.html#playing-a-file,VLC player
|
||||
8ba5ae7a-5ae5-4eab-9fcc-5dd4fe3abf89,https://docs.videolan.me/vlc-user/desktop/3.0/en/basic/recording/playing.html#choose-your-recordings-folder,VLC player
|
||||
8f080098-ddb1-424c-b438-4e96e5e4786e,https://medium.com/@jetscribe_ai/how-to-extract-mp3-audio-from-videos-using-vlc-media-player-beeef644ebfb,VLC player
|
||||
bba3381f-b5eb-4439-bd9e-80c22218d5a7,https://www.quora.com/How-do-I-play-online-videos-using-the-VLC-media-player,VLC player
|
||||
a1c3ab35-02de-4999-a7ed-2fd12c972c6e,https://www.quora.com/How-do-I-compress-a-video-with-VLC,VLC player
|
||||
fba2c100-79e8-42df-ae74-b592418d54f4,https://www.youtube.com/watch?v=XHprwDJ0-fU&t=436s,VLC player
|
||||
d70666e4-7348-42c7-a06a-664094c5df3c,https://www.youtube.com/watch?v=XHprwDJ0-fU&t=436s,VLC player
|
||||
efcf0d81-0835-4880-b2fd-d866e8bc2294,"https://www.youtube.com/watch?v=XHprwDJ0-fU&t=436s, https://help.ubuntu.com/stable/ubuntu-help/look-background.html.en",VLC player
|
||||
8d9fd4e2-6fdb-46b0-b9b9-02f06495c62f,https://www.youtube.com/watch?v=XHprwDJ0-fU&t=436s,VLC player
|
||||
aa4b5023-aef6-4ed9-bdc9-705f59ab9ad6,https://videoconverter.wondershare.com/vlc/how-to-rotate-a-video-using-vlc.html?gad_source=1&gclid=CjwKCAiA-vOsBhAAEiwAIWR0TaGSOLkYiBeVQGZSyfeUP3g-tIvYxffl5RFIu0-zrUL1IF41eCw1JRoCnCMQAvD_BwE,VLC player
|
||||
386dbd0e-0241-4a0a-b6a2-6704fba26b1c,https://superuser.com/questions/1708415/pause-and-play-vlc-in-background?rq=1,VLC player
|
||||
9195653c-f4aa-453d-aa95-787f6ccfaae9,https://superuser.com/questions/1513285/how-can-i-increase-the-maximum-volume-output-by-vlc?rq=1,VLC player
|
||||
5ac2891a-eacd-4954-b339-98abba077adb,"https://superuser.com/questions/1412810/how-to-prevent-vlc-media-player-from-auto-closing-after-video-end#:%7E:text=Click%20on%20%22Media%22on%20the,VLC%20player%20after%20video%20ending",VLC player
|
||||
0d95d28a-9587-433b-a805-1fbe5467d598,https://superuser.com/questions/1299036/vlc-how-to-open-the-folder-of-the-current-playing-video?noredirect=1&lq=1,VLC player
|
||||
d06f0d4d-2cd5-4ede-8de9-598629438c6e,https://superuser.com/questions/1039392/changing-colour-of-vlc-volume-slider,VLC player
|
||||
a5bbbcd5-b398-4c91-83d4-55e1e31bbb81,https://superuser.com/questions/776056/how-to-hide-bottom-toolbar-in-vlc,VLC player
|
||||
f3977615-2b45-4ac5-8bba-80c17dbe2a37,https://www.reddit.com/r/Fedora/comments/rhljzd/how_to_run_multiple_instances_of_vlc_media_player/,VLC player
|
||||
c669a35f-d45a-450e-b1f2-f473748337bb,https://www.quora.com/How-do-I-fast-forward-a-video-in-VLC-player,VLC player
|
||||
d1ba14d0-fef8-4026-8418-5b581dc68ca0,https://superuser.com/questions/306154/how-to-use-a-b-repeat-feature-of-vlc,VLC player
|
||||
215dfd39-f493-4bc3-a027-8a97d72c61bf,https://superuser.com/questions/1224784/how-to-change-vlcs-splash-screen,VLC player
|
||||
bb5e4c0d-f964-439c-97b6-bdb9747de3f4,https://www.wikihow.com/Remove-an-Email-Account-from-Thunderbird,ThunderBird
|
||||
7b6c7e24-c58a-49fc-a5bb-d57b80e5b4c3,https://www.wikihow.com/Access-Gmail-With-Mozilla-Thunderbird,ThunderBird
|
||||
b188fe10-ae67-4db8-a154-26a0b8ff8f1e,https://www.reddit.com/r/Thunderbird/comments/17vv2os/restore_readability_in_message_list_pane/,ThunderBird
|
||||
12086550-11c0-466b-b367-1d9e75b3910e,https://www.bitrecover.com/blog/manage-thunderbird-profiles/,ThunderBird
|
||||
06fe7178-4491-4589-810f-2e2bc9502122,https://www.quora.com/How-do-I-backup-email-files-in-Mozilla-Thunderbird,ThunderBird
|
||||
6766f2b8-8a72-417f-a9e5-56fcaa735837,"https://www.adsigner.com/user-manual/signatures/setup-email-client-thunderbird/#:~:text=is%20probably%20hidden.-,Right%20click%20on%20the%20empty%20space%20at%20the%20top%20of,signature%20from%20a%20file%20instead.",ThunderBird
|
||||
e1e75309-3ddb-4d09-92ec-de869c928143,https://support.mozilla.org/en-US/kb/organize-your-messages-using-filters,ThunderBird
|
||||
3d1682a7-0fb0-49ae-a4dc-a73afd2d06d5,https://support.mozilla.org/en-US/kb/organize-your-messages-using-filters,ThunderBird
|
||||
35253b65-1c19-4304-8aa4-6884b8218fc0,https://support.mozilla.org/en-US/questions/1259354,ThunderBird
|
||||
d088f539-cab4-4f9a-ac92-9999fc3a656e,https://support.mozilla.org/en-US/kb/how-use-attachments,ThunderBird
|
||||
2ad9387a-65d8-4e33-ad5b-7580065a27ca,"https://support.mozilla.org/bm/questions/1027435, https://www.wikihow.tech/Create-Folders-in-Mozilla-Thunderbird",ThunderBird
|
||||
480bcfea-d68f-4aaa-a0a9-2589ef319381,https://www.reddit.com/r/Thunderbird/comments/182dg5p/unified_inbox_howto/,ThunderBird
|
||||
37b9808f-b2b4-4177-ab00-9ddfae4bad27,https://www.quora.com/How-can-I-schedule-Mozilla-Thunderbird-to-turn-off-automatically,ThunderBird
|
||||
af630914-714e-4a24-a7bb-f9af687d3b91,https://stackoverflow.com/questions/11333148/adding-a-toolbar-button-to-a-thundebird-compose-message-window?rq=3,ThunderBird
|
||||
3299584d-8f11-4457-bf4c-ce98f7600250,https://superuser.com/questions/1643561/would-like-to-see-the-email-address-from-sender-in-the-column,ThunderBird
|
||||
030eeff7-b492-4218-b312-701ec99ee0cc,https://superuser.com/questions/1781004/how-do-i-remove-the-indentation-and-character-in-quoted-text-of-a-reply-mess,ThunderBird
|
||||
94760984-3ff5-41ee-8347-cf1af709fea0,https://superuser.com/questions/1757333/how-can-i-view-thunderbird-in-full-dark-mode,ThunderBird
|
||||
99146c54-4f37-4ab8-9327-5f3291665e1e,https://superuser.com/questions/1764409/how-to-send-email-with-thunderbird-without-configuring-an-incoming-email-service,ThunderBird
|
||||
9656a811-9b5b-4ddf-99c7-5117bcef0626,https://superuser.com/questions/205240/is-there-a-way-to-get-a-popup-confirmation-box-when-you-send-an-email-in-thunder?rq=1,ThunderBird
|
||||
c9e7eaf2-b1a1-4efc-a982-721972fa9f02,https://superuser.com/questions/544480/how-to-apply-automatic-message-filters-to-subfolders-too?noredirect=1&lq=1,ThunderBird
|
||||
bb5e4c0d-f964-439c-97b6-bdb9747de3f4,https://support.google.com/chrome/answer/95426?sjid=16867045591165135686-AP,Chrome
|
||||
7b6c7e24-c58a-49fc-a5bb-d57b80e5b4c3,https://support.google.com/chrome/answer/95647?hl=en&ref_topic=7438325&sjid=16867045591165135686-AP#zippy=%2Cdelete-cookies-from-a-site,Chrome
|
||||
12086550-11c0-466b-b367-1d9e75b3910e,https://www.quora.com/What-are-the-cool-tricks-to-use-Google-Chrome,Chrome
|
||||
06fe7178-4491-4589-810f-2e2bc9502122,https://www.wikihow.com/Switch-Tabs-in-Chrome,Chrome
|
||||
6766f2b8-8a72-417f-a9e5-56fcaa735837,https://support.google.com/chrome/thread/205881926/it-s-possible-to-load-unpacked-extension-automatically-in-chrome?hl=en,Chrome
|
||||
e1e75309-3ddb-4d09-92ec-de869c928143,https://in5stepstutorials.com/google-chrome/save-web-page-as-pdf-in-chrome.php,Chrome
|
||||
3d1682a7-0fb0-49ae-a4dc-a73afd2d06d5,https://in5stepstutorials.com/google-chrome/add-change-delete-autofill-address.php,Chrome
|
||||
35253b65-1c19-4304-8aa4-6884b8218fc0,"https://www.laptopmag.com/articles/how-to-create-desktop-shortcuts-for-web-pages-using-chrome, https://www.reddit.com/r/chrome/comments/13xcbap/crete_shortcut_option_missing/",Chrome
|
||||
d088f539-cab4-4f9a-ac92-9999fc3a656e,https://medium.com/@inkverseuk2/useful-tips-and-tricks-for-the-google-chrome-browser-ac7d0d24b3cc,Chrome
|
||||
2ad9387a-65d8-4e33-ad5b-7580065a27ca,https://www.youtube.com/watch?v=IN-Eq_UripQ,Chrome
|
||||
7a5a7856-f1b6-42a4-ade9-1ca81ca0f263,https://www.youtube.com/watch?v=ZaZ8GcTxjXA,Chrome
|
||||
3720f614-37fd-4d04-8a6b-76f54f8c222d,https://superuser.com/questions/984668/change-interface-language-of-chrome-to-english,Chrome
|
||||
b63059a2-53bc-4163-a89f-3ac948c74081,https://superuser.com/questions/1303418/how-do-i-make-chrome-block-absolutely-all-pop-ups?rq=1,Chrome
|
||||
44ee5668-ecd5-4366-a6ce-c1c9b8d4e938,https://superuser.com/questions/1787991/clear-browsing-history-from-specific-site-on-chrome,Chrome
|
||||
b5ebc8c6-6329-4373-85b4-9421c97375e9,https://superuser.com/questions/364470/is-there-a-way-to-view-google-chrome-browsing-history-past-three-months-ago?rq=1,Chrome
|
||||
93eabf48-6a27-4cb6-b963-7d5fe1e0d3a9,https://superuser.com/questions/1417973/how-to-disable-google-chrome-dark-mode,Chrome
|
||||
2ae9ba84-3a0d-4d4c-8338-3a1478dc5fe3,https://superuser.com/questions/1393683/how-to-change-the-username-in-google-chrome-profiles?rq=1,Chrome
|
||||
480bcfea-d68f-4aaa-a0a9-2589ef319381,https://bugartisan.medium.com/disable-the-new-chrome-ui-round-in-2023-db168271f71e,Chrome
|
||||
37b9808f-b2b4-4177-ab00-9ddfae4bad27,https://www.reddit.com/r/chrome/comments/17niw3h/tutorial_how_to_disable_the_download_bubble_in/,Chrome
|
||||
af630914-714e-4a24-a7bb-f9af687d3b91,https://www.howtogeek.com/680260/how-to-change-chromes-default-text-size/,Chrome
|
||||
ae78f875-5b98-4907-bbb5-9c737fc68c03,https://support.google.com/chrome/thread/219988391/increase-search-results-per-page?hl=en,Chrome
|
||||
0ed39f63-6049-43d4-ba4d-5fa2fe04a951,https://www.quora.com/How-do-you-find-and-replace-text-in-Visual-Studio-Code,VS Code
|
||||
b421106e-b282-4c41-af72-37c95493f95f,https://stackoverflow.com/questions/74153883/launch-vscode-with-new-txt-file,VS Code
|
||||
53ad5833-3455-407b-bbc6-45b4c79ab8fb,https://www.youtube.com/watch?v=VqCgcpAypFQ,VS Code
|
||||
eabc805a-bfcf-4460-b250-ac92135819f6,https://www.youtube.com/watch?v=VqCgcpAypFQ,VS Code
|
||||
3486f395-ad68-459c-8c39-ea07de934dd4,https://www.youtube.com/watch?v=VqCgcpAypFQ,VS Code
|
||||
982d12a5-beab-424f-8d38-d2a48429e511,https://www.youtube.com/watch?v=ORrELERGIHs,VS Code
|
||||
4e60007a-f5be-4bfc-9723-c39affa0a6d3,"https://campbell-muscle-lab.github.io/howtos_Python/pages/documentation/best_practices/vscode_docstring_extension/vscode_docstring_extension.html#:~:text=Type%2C%20Ctrl%20%2B%20Shift%20%2B%20P,select%20the%20NumPy%20docstring%20format.",VS Code
|
||||
e2b5e914-ffe1-44d2-8e92-58f8c5d92bb2,https://superuser.com/questions/1386061/how-to-suppress-some-python-errors-warnings-in-vs-code,VS Code
|
||||
9439a27b-18ae-42d8-9778-5f68f891805e,https://stackoverflow.com/questions/75832474/how-to-keep-cursor-in-debug-console-when-debugging-in-visual-studio-code,VS Code
|
||||
ae506c68-352c-4094-9caa-ee9d42052317,https://superuser.com/questions/1460404/get-visual-studio-code-terminal-history?rq=1,VS Code
|
||||
ea98c5d7-3cf9-4f9b-8ad3-366b58e0fcae,https://superuser.com/questions/1748097/vs-code-disable-tree-view-find-explorer-search,VS Code
|
||||
c714dcee-cad3-4e12-8f3c-12bdcfcdb048,https://superuser.com/questions/1417361/how-to-disable-file-filtering-in-vs-code-sidebar-explorer?rq=1,VS Code
|
||||
930fdb3b-11a8-46fe-9bac-577332e2640e,https://superuser.com/questions/1270103/how-to-switch-the-cursor-between-terminal-and-code-in-vscode,VS Code
|
||||
276cc624-87ea-4f08-ab93-f770e3790175,https://www.quora.com/unanswered/How-do-you-set-the-line-length-in-Visual-Studio-Code,VS Code
|
||||
9d425400-e9b2-4424-9a4b-d4c7abac4140,https://superuser.com/questions/1466771/is-there-a-way-to-make-editor-tabs-stack-in-vs-code,VS Code
|
||||
7a4deb26-d57d-4ea9-9a73-630f66a7b568,https://www.quora.com/How-do-I-edit-a-photo-in-GIMP,GIMP
|
||||
554785e9-4523-4e7a-b8e1-8016f565f56a,https://www.quora.com/How-do-I-edit-a-photo-in-GIMP,GIMP
|
||||
77b8ab4d-994f-43ac-8930-8ca087d7c4b4,https://superuser.com/questions/1636113/how-to-get-gimp-to-recognize-images-or-pictures-folder-as-the-default-folder-for,GIMP
|
||||
f4aec372-4fb0-4df5-a52b-79e0e2a5d6ce,https://superuser.com/questions/612338/how-do-i-select-and-move-an-object-in-gimp,GIMP
|
||||
d52d6308-ec58-42b7-a2c9-de80e4837b2b,https://superuser.com/questions/1447106/how-to-get-rid-of-the-gimp-tool-options-box,GIMP
|
||||
2a729ded-3296-423d-aec4-7dd55ed5fbb3,https://www.youtube.com/watch?v=lOzSiOIipSM,GIMP
|
||||
b148e375-fe0b-4bec-90e7-38632b0d73c2,https://www.quora.com/How-do-I-add-layers-in-GIMP,GIMP
|
||||
a746add2-cab0-4740-ac36-c3769d9bfb46,https://www.youtube.com/watch?v=_L_MMU22bAw,GIMP
|
||||
7b7617bd-57cc-468e-9c91-40c4ec2bcb3d,https://www.youtube.com/watch?v=G_PjQAy0iiU,GIMP
|
||||
d16c99dc-2a1e-46f2-b350-d97c86c85c15,https://stackoverflow.com/questions/75185543/use-gimp-to-resize-image-in-one-layer-only,GIMP
|
||||
573f79b5-abfe-4507-b455-251d45fe6198,https://stackoverflow.com/questions/45196895/gimp-add-padding-to-multiple-images,GIMP
|
||||
06ca5602-62ca-47f6-ad4f-da151cde54cc,https://stackoverflow.com/questions/74664666/how-to-export-palette-based-png-in-gimp,GIMP
|
||||
fa9b1e10-4d2d-4a13-af76-7efa822b6a8b,https://stackoverflow.com/questions/24626608/how-to-combine-several-png-images-as-layers-in-a-single-xcf-image,GIMP
|
||||
6b2b72ed-3a10-4849-876a-750f7cdf3886,https://stackoverflow.com/questions/21018007/resize-image-to-fit-canvas-gimp,GIMP
|
||||
d0e42fd2-d290-46b3-b598-a6e2b7be9c85,https://stackoverflow.com/questions/56758689/stop-gimp-from-merging-layers-when-de-selecting,GIMP
|
||||
e2dd0213-26db-4349-abe5-d5667bfd725c,https://superuser.com/questions/839650/how-to-move-an-inserted-text-box-in-gimp,GIMP
|
||||
f723c744-e62c-4ae6-98d1-750d3cd7d79d,https://www.reddit.com/r/GIMP/comments/12e57w8/how_to_use_gimp_to_exaggerate_contrast/,GIMP
|
||||
8d6b1c9c-1aab-47fe-9ba5-e84c838d0c57,https://www.quora.com/How-can-email-attachments-be-converted-into-a-word-document-using-Mozilla-Thunderbird,multiple
|
||||
11e1e614-9696-4d94-88c9-8e556880d41a,https://ifttt.com/applets/L2A89geP-send-chrome-software-update-release-alerts-to-email,multiple
|
||||
57956154-f0fe-486b-88b8-e7126da035a9,https://zapier.com/apps/email/integrations/google-sheets/547/get-email-notifications-for-new-rows-in-a-google-sheets-spreadsheet,multiple
|
||||
ec14c524-b245-456d-abd6-ec12c746e9f8,https://zapier.com/apps/gmail/integrations/google-sheets/2618/save-new-gmail-emails-matching-certain-traits-to-a-google-spreadsheet,multiple
|
||||
cbf5fbda-425e-4619-bcf2-0ea8d4c0bfa3,https://zapier.com/apps/google-sheets/integrations/google-slides/13919/refresh-charts-on-a-google-slides-presentation-when-rows-are-updated-on-google-sheets,multiple
|
||||
a54284d0-7b93-4327-bfcc-3a421516dbdd,https://superuser.com/questions/655622/cannot-drag-images-from-thunderbird-to-word,multiple
|
||||
58565672-7bfe-48ab-b828-db349231de6b,https://superuser.com/questions/1792660/open-link-from-other-application-does-not-open-the-url-in-firefox,multiple
|
||||
6d72aad6-187a-4392-a4c4-ed87269c51cf,https://superuser.com/questions/923171/converting-openoffice-impress-presentation-to-video-without-screen-recording,multiple
|
||||
937087b6-f668-4ba6-9110-60682ee33441,https://superuser.com/questions/187440/set-default-ubuntu-video-player-as-vlc,multiple
|
||||
f8cfa149-d1c1-4215-8dac-4a0932bad3c2,https://superuser.com/questions/1803088/libreoffice-calc-clears-clipboard,multiple
|
||||
5e974913-6905-4c3f-8b65-d7837f3931cc,https://stackoverflow.com/questions/61856141/how-can-i-start-thunderbird-and-minimize-the-window-on-startup-in-ubuntu,multiple
|
||||
7c179dad-f1c7-4892-b53f-d1c4023d23c7,https://stackoverflow.com/questions/21155085/pasting-excel-tables-in-thunderbird-e-mail-client,multiple
|
||||
4a68b2dd-70f2-4532-9bc1-d21878bd8cb2,https://stackoverflow.com/questions/65669955/thunderbird-how-to-send-a-mail-to-all-receivers-of-a-folder,multiple
|
||||
c8457fde-b14b-4aba-b402-144842ea29e1,https://stackoverflow.com/questions/65788200/how-to-open-xlsx-files-in-ms-excel-from-vs-code,multiple
|
||||
81c425f5-78f3-4771-afd6-3d2973825947,https://www.zyxware.com/articles/3770/how-to-transfer-data-in-libreoffice-calc-to-libreoffice-writer-in-table-format,multiple
|
||||
bb83cab4-e5c7-42c7-a67b-e46068032b86,https://ask.libreoffice.org/t/save-impress-presentation-as-writer-document/5291/4,multiple
|
||||
227d2f97-562b-4ccb-ae47-a5ec9e142fbb,https://discourse.gnome.org/t/gimp-and-libre-office-writer/15430/4,multiple
|
||||
a6bbc08c-51e9-4ee4-9327-83d05075d960,https://forum.openoffice.org/en/forum/viewtopic.php?t=105055,multiple
|
||||
964e6e03-ba31-466b-8c15-5a351a81f675,https://www.maketecheasier.com/mail-merge-thunderbird-calc/,multiple
|
||||
2fe4b718-3bd7-46ec-bdce-b184f5653624,https://www.thewindowsclub.com/how-to-create-animated-gif-from-a-video-file-using-vlc-and-gimp,multiple
|
||||
d02b9364-6bb0-4c7e-9dbd-4db62822bc26,https://stackoverflow.com/questions/38306910/simple-python-script-to-get-a-libreoffice-base-field-and-play-on-vlc,multiple
|
||||
57fb469b-127a-46fa-8281-bbb3840efdf5,https://support.mozilla.org/en-US/questions/1150626,multiple
|
||||
3680a5ee-6870-426a-a997-eba929a0d25c,https://unix.stackexchange.com/questions/510850/how-to-open-calc-from-terminal-and-insert-files,multiple
|
||||
2d8c8a20-6f54-4c2e-ad56-61fbe7af6b78,https://www.quora.com/How-do-I-force-LibreOffice-Calc-to-recalculate-a-spreadsheet-from-the-command-line,multiple
|
||||
ee9a3c83-f437-4879-8918-be5efbb9fac7,https://stackoverflow.com/questions/64589140/convert-ods-to-csv-using-command-line-when-libreoffice-instance-is-running,multiple
|
||||
f7dfbef3-7697-431c-883a-db8583a4e4f9,https://www.thegeekdiary.com/libreoffice-command-examples-in-linux/,multiple
|
||||
2b9493d7-49b8-493a-a71b-56cd1f4d6908,https://devicetests.com/kill-libreoffice-writer-command-line-ubuntu,multiple
|
||||
51f5801c-18b3-4f25-b0c3-02f85507a078,https://github.com/danielrcollins1/ImpressExtractNotes,multiple
|
||||
81de345e-5473-4cb6-a74d-b6abf3475a6a,https://stackoverflow.com/questions/45588952/how-can-i-compose-and-send-email-in-thunderbird-from-commandline,multiple
|
||||
2c9fc0de-3ee7-45e1-a5df-c86206ad78b5,https://nikki-ricks.medium.com/how-to-use-git-add-commit-and-push-in-vs-code-and-command-line-35c0e8c47b62,multiple
|
||||
510f64c8-9bcc-4be1-8d30-638705850618,https://www.geeksforgeeks.org/how-to-start-vs-code-from-the-terminal-command-line/,multiple
|
||||
9ff484f7-5c09-4398-ae29-d5904e59e138,https://stackoverflow.com/questions/38606973/playing-opening-and-pausing-vlc-command-line-executed-from-python-scripts,multiple
|
||||
d9b7c649-c975-4f53-88f5-940b29c47247,https://marketplace.uipath.com/listings/extract-the-first-1000-gmail-emails-from-the-current-month-in-a-new-google-sheets-report,multiple
|
||||
be4ef0dc-0f70-4936-81d8-3cd2b04482f8,https://marketplace.uipath.com/listings/table-data-extraction-for-sales-opportunities-to-excel-workbook,multiple
|
||||
78aed49a-a710-4321-a793-b611a7c5b56b,https://marketplace.uipath.com/listings/upload-email-attachments-from-gmail-to-google-drive,multiple
|
||||
897e3b53-5d4d-444b-85cb-2cdc8a97d903,https://marketplace.uipath.com/listings/convert-word-file-to-pdf-and-store-in-onedrive,multiple
|
||||
4e9f0faf-2ecc-4ae8-a804-28c9a75d1ddc,https://marketplace.uipath.com/listings/extract-data-from-a-new-invoice-file-in-google-drive-and-store-it-in-google-sheets4473,multiple
|
||||
b52b40a5-ad70-4c53-b5b0-5650a8387052,https://marketplace.uipath.com/listings/merge-pdfs-from-gmail-email-attachments-and-upload-to-gogle-drive,multiple
|
||||
46407397-a7d5-4c6b-92c6-dbe038b1457b,https://marketplace.uipath.com/listings/upload-to-google-drive-images-from-pdf-attachments-received-via-gmail,multiple
|
||||
a0b9dc9c-fc07-4a88-8c5d-5e3ecad91bcb,https://marketplace.uipath.com/listings/backup-important-emails-to-onedrive-or-sharepoint,multiple
|
||||
665f4af1-617d-4009-baff-84ff66071e6a,https://www.howtogeek.com/663927/how-to-open-google-chrome-using-command-prompt-on-windows-10/#open-chrome-straight-to-a-specific-website,multiple
|
||||
e6313b30-3903-4ed9-8c7d-4c47bf51fc96,https://stackoverflow.com/questions/12258086/how-do-i-run-google-chrome-as-root,multiple
|
||||
|
@@ -1,238 +0,0 @@
|
||||
import csv
|
||||
import os
|
||||
import yt_dlp
|
||||
from docx import Document
|
||||
import requests
|
||||
from bs4 import BeautifulSoup
|
||||
from PIL import Image
|
||||
import pytesseract
|
||||
from io import BytesIO
|
||||
from docx import Document
|
||||
import re
|
||||
import markdownify
|
||||
from markdownify import markdownify as md
|
||||
|
||||
def valid_xml_char_ordinal(c):
|
||||
codepoint = ord(c)
|
||||
# conditions ordered by presumed frequency
|
||||
return (
|
||||
0x20 <= codepoint <= 0xD7FF or
|
||||
codepoint in (0x9, 0xA, 0xD) or
|
||||
0xE000 <= codepoint <= 0xFFFD or
|
||||
0x10000 <= codepoint <= 0x10FFFF
|
||||
)
|
||||
|
||||
def download_and_clean_youtube_subtitles(video_url, txt_filepath):
|
||||
# set up youtube-dl options to download the subtitles
|
||||
subtitles_path = txt_filepath[0:-4]
|
||||
ydl_opts = {
|
||||
'skip_download': True,
|
||||
'writesubtitles': True,
|
||||
'writeautomaticsub': True, # if no subtitles are available, try to generate them
|
||||
'subtitleslangs': ['en'],
|
||||
'outtmpl': f'{subtitles_path}.%(ext)s',
|
||||
'quiet': True,
|
||||
}
|
||||
|
||||
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
|
||||
# download the subtitles
|
||||
ydl.download([video_url])
|
||||
subtitle_file = f'{subtitles_path}.en.vtt'
|
||||
|
||||
# read the subtitle file
|
||||
subtitles = []
|
||||
try:
|
||||
with open(subtitle_file, 'r', encoding='utf-8') as file:
|
||||
lines = file.readlines()
|
||||
|
||||
# define a pattern to match the time line
|
||||
pattern = re.compile(r'(\d{2}:\d{2}:\d{2}.\d{3} --> \d{2}:\d{2}:\d{2}.\d{3})|(^WEBVTT)|(^Kind: captions)|(^Language: .*)')
|
||||
|
||||
# clean the subtitles
|
||||
for line in lines:
|
||||
# if this line is a time line or it is blank , skip it
|
||||
if pattern.match(line) or line.strip() == '':
|
||||
continue
|
||||
# add this subtitle line to subtitles list, remove the trailing spaces and line change
|
||||
subtitles.append(line.strip())
|
||||
|
||||
# remove duplicated subtitles
|
||||
subtitles = list(dict.fromkeys(subtitles))
|
||||
|
||||
# save the subtitles as a txt file
|
||||
with open(txt_filepath, 'w', encoding='utf-8') as f:
|
||||
for line in subtitles:
|
||||
if line:
|
||||
f.write(line + '\n')
|
||||
|
||||
except IOError:
|
||||
print(f"Could not read file: {subtitle_file}")
|
||||
|
||||
# scrape a webpage and perform OCR on images
|
||||
def scrape_and_ocr_forum(url, doc):
|
||||
response = requests.get(url)
|
||||
soup = BeautifulSoup(response.content, 'html.parser')
|
||||
|
||||
text_elements = soup.find_all(['h1', 'h2', 'h3', 'p', 'li'])
|
||||
for element in text_elements:
|
||||
doc.add_paragraph(element.get_text())
|
||||
|
||||
image_elements = soup.find_all('img')
|
||||
for image in image_elements:
|
||||
if 'src' not in image.attrs:
|
||||
continue
|
||||
image_url = image['src']
|
||||
if image_url.startswith('http'):
|
||||
if not image_url.endswith('.svg') and not image_url.endswith('.png'):
|
||||
continue
|
||||
if 'neveragain.allstatics.com/2019/assets/icon/logo' in image_url:
|
||||
continue
|
||||
img_response = requests.get(image_url, stream=True)
|
||||
img = Image.open(BytesIO(img_response.content))
|
||||
ocr_text = pytesseract.image_to_string(img)
|
||||
|
||||
if ocr_text != ' ' and ocr_text != '':
|
||||
cleaned_string = ''.join(c for c in ocr_text if valid_xml_char_ordinal(c))
|
||||
doc.add_paragraph(cleaned_string)
|
||||
|
||||
def superuser_to_markdown(url, doc_filepath):
|
||||
response = requests.get(url)
|
||||
soup = BeautifulSoup(response.content, 'html.parser')
|
||||
|
||||
# set up the markdown document
|
||||
markdown_content = ""
|
||||
|
||||
# get the question title and body
|
||||
question_title = soup.find('h1').get_text(strip=True)
|
||||
question = soup.find('div', {'id': 'question'})
|
||||
if question:
|
||||
question_body = question.find('div', {'class': 's-prose js-post-body'}).prettify()
|
||||
markdown_content += f"# {question_title}\n\n" + markdownify.markdownify(question_body, heading_style="ATX") + "\n\n"
|
||||
|
||||
# get all answers
|
||||
answers = soup.find_all('div', {'class': 'answer'})
|
||||
for answer in answers:
|
||||
answer_body = answer.find('div', {'class': 's-prose js-post-body'}).prettify()
|
||||
markdown_content += markdownify.markdownify(answer_body, heading_style="ATX") + "\n\n"
|
||||
|
||||
# deal with images and perform OCR
|
||||
all_img_tags = question.find_all('img') + [img for answer in answers for img in answer.find_all('img')]
|
||||
for img_tag in all_img_tags:
|
||||
image_src = img_tag.get('src') or img_tag.get('data-src') # Superuser uses lazy loading
|
||||
if image_src and image_src.startswith('http'):
|
||||
img_response = requests.get(image_src, stream=True)
|
||||
img = Image.open(BytesIO(img_response.content))
|
||||
ocr_text = pytesseract.image_to_string(img)
|
||||
if ocr_text.strip(): # if the OCR result is not empty, add it to the markdown content
|
||||
markdown_content += "```\n" + ocr_text.strip() + "\n```\n\n"
|
||||
|
||||
with open(doc_filepath, 'w', encoding='utf-8') as f:
|
||||
f.write(markdown_content)
|
||||
|
||||
|
||||
def stack_overflow_to_markdown(url, doc_filepath):
|
||||
response = requests.get(url)
|
||||
soup = BeautifulSoup(response.content, 'html.parser')
|
||||
|
||||
# set up the markdown document
|
||||
markdown_content = ""
|
||||
|
||||
# get the question title and body
|
||||
question = soup.find('div', {'id': 'question'})
|
||||
|
||||
question_title = soup.find('h1').get_text(strip=True)
|
||||
if question:
|
||||
|
||||
question_body = question.find('div', {'class': 's-prose js-post-body'}).prettify()
|
||||
markdown_content += f"# {question_title}\n\n" + markdownify.markdownify(question_body, heading_style="ATX") + "\n\n"
|
||||
|
||||
# get all answers
|
||||
answers = soup.find_all('div', {'class': 'answer'})
|
||||
for answer in answers:
|
||||
answer_body = answer.find('div', {'class': 's-prose js-post-body'}).prettify()
|
||||
markdown_content += markdownify.markdownify(answer_body, heading_style="ATX") + "\n\n"
|
||||
|
||||
# deal with images and perform OCR
|
||||
all_img_tags = soup.find_all('img')
|
||||
for img_tag in all_img_tags:
|
||||
image_url = img_tag['src']
|
||||
if image_url.startswith('http') and (image_url.endswith('.svg') or image_url.endswith('.png')): # 确保图片URL有效
|
||||
img_response = requests.get(image_url, stream=True)
|
||||
img = Image.open(BytesIO(img_response.content))
|
||||
ocr_text = pytesseract.image_to_string(img)
|
||||
if ocr_text.strip():
|
||||
markdown_content += "```\n" + ocr_text.strip() + "\n```\n\n"
|
||||
|
||||
with open(doc_filepath, 'w', encoding='utf-8') as f:
|
||||
f.write(markdown_content)
|
||||
|
||||
def scrape_webpage_to_markdown(url, doc_filepath):
|
||||
response = requests.get(url)
|
||||
soup = BeautifulSoup(response.content, 'html.parser')
|
||||
|
||||
articles = soup.find_all('article') or soup.find_all('main') or soup.find_all('div', {'class': 'lia-message-body-content'})
|
||||
|
||||
if not articles:
|
||||
return
|
||||
|
||||
markdown_content = ''
|
||||
|
||||
# scrape the webpage and perform OCR on images
|
||||
for article in articles:
|
||||
for child in article.recursiveChildGenerator():
|
||||
# if this is an image, perform OCR
|
||||
if child.name == 'img':
|
||||
img_url = child.get('src')
|
||||
if not img_url.startswith(('http:', 'https:')):
|
||||
img_url = '{}{}'.format(url, img_url)
|
||||
if not img_url.endswith('.svg') and not img_url.endswith('.png'):
|
||||
continue
|
||||
if 'neveragain.allstatics.com/2019/assets/icon/logo' in img_url:
|
||||
continue
|
||||
try:
|
||||
img_response = requests.get(img_url, stream=True)
|
||||
img = Image.open(BytesIO(img_response.content))
|
||||
ocr_text = pytesseract.image_to_string(img)
|
||||
if ocr_text.strip():
|
||||
markdown_content += '\n```plaintext\n{}\n```\n'.format(ocr_text.strip())
|
||||
continue
|
||||
except PIL.UnidentifiedImageError:
|
||||
print("unidentified image")
|
||||
|
||||
# Not an image, so continue recursively calling function
|
||||
if child.name is None:
|
||||
continue
|
||||
|
||||
html_str = str(child)
|
||||
markdown_content += md(html_str) + '\n\n'
|
||||
|
||||
with open(doc_filepath, 'w', encoding='utf-8') as f:
|
||||
f.write(markdown_content)
|
||||
|
||||
|
||||
# process a URL and save the file
|
||||
def process_url(url, doc_id, app):
|
||||
doc_filepath = f"/content/drive/MyDrive/SourceDoc/{doc_id}_{app}.md"
|
||||
txt_filepath = f"/content/drive/MyDrive/SourceDoc/{doc_id}_{app}.txt"
|
||||
doc = Document()
|
||||
|
||||
if 'youtube.com' in url or 'youtu.be' in url:
|
||||
download_and_clean_youtube_subtitles(url, txt_filepath)
|
||||
elif 'superuser.com' in url:
|
||||
superuser_to_markdown(url, doc_filepath)
|
||||
elif 'stackoverflow.com' in url:
|
||||
stack_overflow_to_markdown(url, doc_filepath)
|
||||
else:
|
||||
scrape_webpage_to_markdown(url, doc_filepath)
|
||||
|
||||
# read the CSV file and process each URL
|
||||
csv_filepath = './Get_Source_Doc - Sheet1.csv'
|
||||
with open(csv_filepath, 'r', newline='', encoding='utf-8') as csvfile:
|
||||
reader = csv.DictReader(csvfile)
|
||||
cnt = 55
|
||||
for row in reader:
|
||||
if cnt>0:
|
||||
cnt -= 1
|
||||
continue
|
||||
process_url(row['Source'], row['id'], row['InvolvedApp'])
|
||||
print(row)
|
||||
@@ -1,293 +0,0 @@
|
||||
import csv
|
||||
import os
|
||||
import io
|
||||
import fitz
|
||||
import yt_dlp
|
||||
from docx import Document
|
||||
import requests
|
||||
from bs4 import BeautifulSoup
|
||||
from PIL import Image
|
||||
import pytesseract
|
||||
from io import BytesIO
|
||||
from docx import Document
|
||||
import re
|
||||
import markdownify
|
||||
from markdownify import markdownify as md
|
||||
|
||||
def download_pdf(url):
|
||||
response = requests.get(url)
|
||||
response.raise_for_status() # 确保请求是成功的
|
||||
return io.BytesIO(response.content)
|
||||
|
||||
def pdf_to_markdown(pdf_stream, markdown_path):
|
||||
document = fitz.open(stream=pdf_stream, filetype="pdf")
|
||||
markdown_content = ""
|
||||
|
||||
for page_number in range(len(document)):
|
||||
page = document[page_number]
|
||||
text = page.get_text()
|
||||
markdown_content += text + "\n\n"
|
||||
|
||||
# 提取图片并添加到 Markdown 文件。图片被保存在同一目录下
|
||||
image_list = page.get_images(full=True)
|
||||
if image_list:
|
||||
markdown_content += f"### Page {page_number + 1} Images\n"
|
||||
|
||||
for img_index, image in enumerate(image_list, start=1):
|
||||
# 提取图片
|
||||
xref = image[0]
|
||||
base64_image = document.extract_image(xref)
|
||||
image_bytes = base64_image["image"]
|
||||
|
||||
# 写入图片到磁盘
|
||||
image_filename = f"output_image_page_{page_number + 1}_{img_index}.png"
|
||||
image_abs_path = os.path.join(os.path.dirname(markdown_path), image_filename)
|
||||
with open(image_abs_path, "wb") as image_file:
|
||||
image_file.write(image_bytes)
|
||||
|
||||
# 在 Markdown 文件中添加图片引用
|
||||
markdown_content += f"\n\n"
|
||||
|
||||
with open(markdown_path, "w", encoding="utf-8") as md_file:
|
||||
md_file.write(markdown_content)
|
||||
|
||||
document.close()
|
||||
|
||||
def valid_xml_char_ordinal(c):
|
||||
codepoint = ord(c)
|
||||
# conditions ordered by presumed frequency
|
||||
return (
|
||||
0x20 <= codepoint <= 0xD7FF or
|
||||
codepoint in (0x9, 0xA, 0xD) or
|
||||
0xE000 <= codepoint <= 0xFFFD or
|
||||
0x10000 <= codepoint <= 0x10FFFF
|
||||
)
|
||||
|
||||
def download_and_clean_youtube_subtitles(video_url, txt_filepath):
|
||||
# 设置yt-dlp库的选项来下载字幕
|
||||
subtitles_path = txt_filepath[0:-4]
|
||||
ydl_opts = {
|
||||
'skip_download': True,
|
||||
'writesubtitles': True,
|
||||
'writeautomaticsub': True, # 如果视频没有字幕,尝试下载自动生成的字幕
|
||||
'subtitleslangs': ['en'], # 下载英文字幕
|
||||
'outtmpl': f'{subtitles_path}.%(ext)s', # 确保保存到可写目录
|
||||
'quiet': True,
|
||||
}
|
||||
|
||||
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
|
||||
# 获取视频信息,下载字幕文件
|
||||
ydl.download([video_url])
|
||||
subtitle_file = f'{subtitles_path}.en.vtt'
|
||||
|
||||
# 读取下载的字幕文件
|
||||
subtitles = []
|
||||
try:
|
||||
with open(subtitle_file, 'r', encoding='utf-8') as file:
|
||||
# 读取所有行
|
||||
lines = file.readlines()
|
||||
|
||||
# 正则表达式匹配时间戳和其他不相关的标记
|
||||
pattern = re.compile(r'(\d+:\d\d:\d\d.\d+ --> \d+:\d\d:\d\d.\d+)|(\s*<[^>]+>)')
|
||||
|
||||
# 去除时间戳和HTML标签等,只保留字幕文本
|
||||
lines = [re.sub(pattern, '', line).strip() for line in lines if line.strip() and not pattern.match(line)]
|
||||
|
||||
# 清洗字幕
|
||||
for line in lines:
|
||||
# 如果这是一个时间线或者其他不需要的信息,跳过它
|
||||
if pattern.match(line) or line.strip() == '':
|
||||
continue
|
||||
# 添加到字幕列表,同时去除愈加和前导空白符
|
||||
subtitles.append(line.strip())
|
||||
|
||||
# 去除可能的重复行
|
||||
subtitles = list(dict.fromkeys(subtitles))
|
||||
|
||||
# 保存至txt文件
|
||||
with open(txt_filepath, 'w', encoding='utf-8') as f:
|
||||
for line in subtitles:
|
||||
if line: # 避免写入空行
|
||||
f.write(line + '\n')
|
||||
|
||||
except IOError:
|
||||
print(f"Could not read file: {subtitle_file}")
|
||||
|
||||
# 爬取论坛内容,对图片进行OCR处理,并保存为.docx文件
|
||||
def scrape_and_ocr_forum(url, doc):
|
||||
response = requests.get(url)
|
||||
soup = BeautifulSoup(response.content, 'html.parser')
|
||||
|
||||
text_elements = soup.find_all(['h1', 'h2', 'h3', 'p', 'li'])
|
||||
for element in text_elements:
|
||||
doc.add_paragraph(element.get_text())
|
||||
|
||||
image_elements = soup.find_all('img')
|
||||
for image in image_elements:
|
||||
if 'src' not in image.attrs:
|
||||
continue
|
||||
image_url = image['src']
|
||||
if image_url.startswith('http'):
|
||||
if not image_url.endswith('.svg') and not image_url.endswith('.png'):
|
||||
continue
|
||||
if 'neveragain.allstatics.com/2019/assets/icon/logo' in image_url:
|
||||
continue
|
||||
img_response = requests.get(image_url, stream=True)
|
||||
img = Image.open(BytesIO(img_response.content))
|
||||
ocr_text = pytesseract.image_to_string(img)
|
||||
|
||||
if ocr_text != ' ' and ocr_text != '':
|
||||
cleaned_string = ''.join(c for c in ocr_text if valid_xml_char_ordinal(c))
|
||||
doc.add_paragraph(cleaned_string)
|
||||
|
||||
def superuser_to_markdown(url, doc_filepath):
|
||||
response = requests.get(url)
|
||||
soup = BeautifulSoup(response.content, 'html.parser')
|
||||
|
||||
# 创建Markdown文件的内容
|
||||
markdown_content = ""
|
||||
|
||||
# 获取问题标题和内容
|
||||
question_title = soup.find('h1').get_text(strip=True)
|
||||
question = soup.find('div', {'id': 'question'})
|
||||
if question:
|
||||
|
||||
question_body = question.find('div', {'class': 's-prose js-post-body'}).prettify()
|
||||
markdown_content += f"# {question_title}\n\n" + markdownify.markdownify(question_body, heading_style="ATX") + "\n\n"
|
||||
|
||||
# 获取所有回答
|
||||
answers = soup.find_all('div', {'class': 'answer'})
|
||||
for answer in answers:
|
||||
answer_body = answer.find('div', {'class': 's-prose js-post-body'}).prettify()
|
||||
markdown_content += markdownify.markdownify(answer_body, heading_style="ATX") + "\n\n"
|
||||
|
||||
# 处理图片并执行OCR
|
||||
all_img_tags = question.find_all('img') + [img for answer in answers for img in answer.find_all('img')]
|
||||
for img_tag in all_img_tags:
|
||||
image_src = img_tag.get('src') or img_tag.get('data-src') # Superuser使用延迟加载的图片
|
||||
if image_src and image_src.startswith('http'):
|
||||
img_response = requests.get(image_src, stream=True)
|
||||
img = Image.open(BytesIO(img_response.content))
|
||||
ocr_text = pytesseract.image_to_string(img)
|
||||
if ocr_text.strip(): # 如果OCR结果非空,则添加到Markdown内容中
|
||||
markdown_content += "```\n" + ocr_text.strip() + "\n```\n\n"
|
||||
|
||||
# 将Markdown内容写入文件
|
||||
with open(doc_filepath, 'w', encoding='utf-8') as f:
|
||||
f.write(markdown_content)
|
||||
|
||||
|
||||
def stack_overflow_to_markdown(url, doc_filepath):
|
||||
response = requests.get(url)
|
||||
soup = BeautifulSoup(response.content, 'html.parser')
|
||||
|
||||
# 创建Markdown文件的内容
|
||||
markdown_content = ""
|
||||
|
||||
# 获取问题标题和内容
|
||||
question = soup.find('div', {'id': 'question'})
|
||||
|
||||
question_title = soup.find('h1').get_text(strip=True)
|
||||
if question:
|
||||
|
||||
question_body = question.find('div', {'class': 's-prose js-post-body'}).prettify()
|
||||
markdown_content += f"# {question_title}\n\n" + markdownify.markdownify(question_body, heading_style="ATX") + "\n\n"
|
||||
|
||||
# 获取所有回答
|
||||
answers = soup.find_all('div', {'class': 'answer'})
|
||||
for answer in answers:
|
||||
answer_body = answer.find('div', {'class': 's-prose js-post-body'}).prettify()
|
||||
markdown_content += markdownify.markdownify(answer_body, heading_style="ATX") + "\n\n"
|
||||
|
||||
# 处理图片并执行OCR
|
||||
all_img_tags = soup.find_all('img')
|
||||
for img_tag in all_img_tags:
|
||||
image_url = img_tag['src']
|
||||
if image_url.startswith('http') and (image_url.endswith('.svg') or image_url.endswith('.png')): # 确保图片URL有效
|
||||
img_response = requests.get(image_url, stream=True)
|
||||
img = Image.open(BytesIO(img_response.content))
|
||||
ocr_text = pytesseract.image_to_string(img)
|
||||
if ocr_text.strip(): # 如果OCR结果非空,则添加到Markdown内容中
|
||||
markdown_content += "```\n" + ocr_text.strip() + "\n```\n\n"
|
||||
|
||||
# 将Markdown内容写入文件
|
||||
with open(doc_filepath, 'w', encoding='utf-8') as f:
|
||||
f.write(markdown_content)
|
||||
|
||||
def scrape_webpage_to_markdown(url, doc_filepath):
|
||||
response = requests.get(url)
|
||||
soup = BeautifulSoup(response.content, 'html.parser')
|
||||
|
||||
# 假设文章内容在 HTML 的 'article' 标签中,根据实际页面结构调整
|
||||
articles = soup.find_all('article') or soup.find_all('main') or soup.find_all('div', {'id':'steps'}, {'class':'section_text'}) # 或其他包含主要内容的HTML标签
|
||||
|
||||
if not articles:
|
||||
articles = soup.find_all('div', {'class': 'lia-message-body-content'})
|
||||
|
||||
markdown_content = ''
|
||||
|
||||
# 抓取所有图文信息
|
||||
for article in articles:
|
||||
for child in article.recursiveChildGenerator():
|
||||
# 如果是图片,则进行OCR
|
||||
if child.name == 'img':
|
||||
img_url = child.get('src')
|
||||
if not img_url:
|
||||
continue
|
||||
if not img_url.startswith(('http:', 'https:')):
|
||||
img_url = '{}{}'.format(url, img_url)
|
||||
if not img_url.endswith('.svg') and not img_url.endswith('.png'):
|
||||
continue
|
||||
if 'neveragain.allstatics.com/2019/assets/icon/logo' in img_url:
|
||||
continue
|
||||
print(img_url)
|
||||
try:
|
||||
img_response = requests.get(img_url, stream=True)
|
||||
img = Image.open(BytesIO(img_response.content))
|
||||
ocr_text = pytesseract.image_to_string(img)
|
||||
if ocr_text.strip():
|
||||
markdown_content += '\n```plaintext\n{}\n```\n'.format(ocr_text.strip())
|
||||
continue
|
||||
except PIL.UnidentifiedImageError:
|
||||
print("unidentified image")
|
||||
# 不是标签,可能是NavigableString或其他
|
||||
if child.name is None:
|
||||
continue
|
||||
# 抓取标签并转换为Markdown
|
||||
html_str = str(child)
|
||||
markdown_content += md(html_str) + '\n\n'
|
||||
|
||||
# 写入markdown文件
|
||||
with open(doc_filepath, 'w', encoding='utf-8') as f:
|
||||
f.write(markdown_content)
|
||||
|
||||
|
||||
# 处理单个URL
|
||||
def process_url(url, doc_id, app):
|
||||
doc_filepath = f"/content/drive/MyDrive/SourceDoc/{doc_id}_{app}.md"
|
||||
txt_filepath = f"/content/drive/MyDrive/SourceDoc/{doc_id}_{app}.txt"
|
||||
doc = Document()
|
||||
|
||||
if 'youtube.com' in url or 'youtu.be' in url:
|
||||
download_and_clean_youtube_subtitles(url, txt_filepath)
|
||||
elif url.endswith('.pdf'):
|
||||
pdf_stream = download_pdf(url)
|
||||
pdf_to_markdown(pdf_stream, doc_filepath)
|
||||
elif 'superuser.com' in url or 'askubuntu.com' in url:
|
||||
superuser_to_markdown(url, doc_filepath)
|
||||
elif 'stackoverflow.com' in url:
|
||||
stack_overflow_to_markdown(url, doc_filepath)
|
||||
else:
|
||||
scrape_webpage_to_markdown(url, doc_filepath)
|
||||
|
||||
# 读取CSV文件中的数据并执行对应操作
|
||||
csv_filepath = '/content/Get_Source_Doc - Sheet1.csv' # 更新为你的CSV文件实际路径
|
||||
with open(csv_filepath, 'r', newline='', encoding='utf-8') as csvfile:
|
||||
reader = csv.DictReader(csvfile)
|
||||
cnt = 176
|
||||
for row in reader:
|
||||
if cnt>0:
|
||||
cnt -= 1
|
||||
continue
|
||||
process_url(row['Source'], row['id'], row['InvolvedApp'])
|
||||
print(row)
|
||||
@@ -1,17 +0,0 @@
|
||||
import requests
|
||||
import json
|
||||
|
||||
url = "https://tiktok-download-video1.p.rapidapi.com/feedSearch"
|
||||
|
||||
querystring = {"keywords":"Ubuntu desktop","count":"10","cursor":"0","region":"US","publish_time":"0","sort_type":"1"}
|
||||
|
||||
headers = {
|
||||
"X-RapidAPI-Key": "YOUR_API_KEY",
|
||||
"X-RapidAPI-Host": "tiktok-download-video1.p.rapidapi.com"
|
||||
}
|
||||
|
||||
response = requests.get(url, headers=headers, params=querystring)
|
||||
|
||||
print(response.json())
|
||||
with open("./record_1.json", "w") as f:
|
||||
json.dump(response.json(), f)
|
||||
@@ -1,506 +0,0 @@
|
||||
{
|
||||
"code": 0,
|
||||
"msg": "success",
|
||||
"processed_time": 1.2181,
|
||||
"data": {
|
||||
"videos": [
|
||||
{
|
||||
"aweme_id": "v0f025gc0000cfdgkhbc77ufdvjieor0",
|
||||
"video_id": "7195358717750070555",
|
||||
"region": "ID",
|
||||
"title": "#CapCut comparing the pros and cons of two operating system: #windows and #linux. \ud83d\udc68\u200d\ud83d\udcbb Unleash the power of #Linux ! Experience a better, more efficient computing with this open-source OS #windows11 #windows10 #ubuntu #programming #computerscience #opensource #technology #FaktaProgrammer #jagocoding #debian #malware #hacking #ProgrammerImut ",
|
||||
"cover": "https://p16-sign-useast2a.tiktokcdn.com/tos-useast2a-p-0037-aiso/2410a4ce346f409ca1f0ba4b543ce7c3_1675300016~tplv-dmt-logom:tos-useast2a-pv-0037-aiso/77578d1e8c9a4efaaa057636e35ab262.image?x-expires=1703127600&x-signature=9GzGgxD2brvooV5TsmsuyCFqloU%3D&s=SEARCH&se=false&sh=&sc=dynamic_cover&l=2023122003034909B448A5A66536000675",
|
||||
"origin_cover": "https://p16-sign-useast2a.tiktokcdn.com/tos-useast2a-p-0037-aiso/5904cc8b48a14d4081f05e97dd006328_1675300016~tplv-tiktokx-360p.webp?x-expires=1703127600&x-signature=zKuLeet7txU5bAPZCeWTFRYHZas%3D&s=SEARCH&se=false&sh=&sc=feed_cover&l=2023122003034909B448A5A66536000675",
|
||||
"duration": 15,
|
||||
"play": "https://v16m.tiktokcdn-us.com/385a61f83cddd3276b6b3955ae059c98/6582ae05/video/tos/useast2a/tos-useast2a-pve-0037-aiso/oIYbF3pDWeVcQUPIUB8jADjnQ2PNBDBLbw2Cne/?a=1233&ch=0&cr=13&dr=0&lr=all&cd=0%7C0%7C0%7C&cv=1&br=1398&bt=699&bti=NEBzNTY6QGo6OjZALnAjNDQuYCMxNDNg&cs=0&ds=6&ft=kJrRfy7oZNx0PD1J-wtxg9w8HGM5kEeC~&mime_type=video_mp4&qs=0&rc=NDQzaDw1N2Q8OTpoNzNkPEBpM3JseWk6ZmtqaTMzZjgzM0BjM2EtXzRgNjIxXjJeNWJhYSNvYmdhcjRfaGFgLS1kL2Nzcw%3D%3D&l=2023122003034909B448A5A66536000675&btag=e00008000",
|
||||
"wmplay": "https://v16m.tiktokcdn-us.com/f6c45472d69d11105cd4f55ebb8e7d13/6582ae05/video/tos/useast2a/tos-useast2a-pve-0037c001-aiso/oI2pBQPnjBbeYpn8DBFwAAI3CDcbb27QeVUKnF/?a=1233&ch=0&cr=13&dr=0&lr=all&cd=0%7C0%7C0%7C&cv=1&br=1124&bt=562&bti=NEBzNTY6QGo6OjZALnAjNDQuYCMxNDNg&cs=0&ds=6&ft=kJrRfy7oZNx0PD1J-wtxg9w8HGM5kEeC~&mime_type=video_mp4&qs=11&rc=ZzVlODZnZWdmM2k2OzhmZ0BpM3JseWk6ZmtqaTMzZjgzM0AtX2BfLjJgX2AxYGJjNDQzYSNvYmdhcjRfaGFgLS1kL2Nzcw%3D%3D&l=2023122003034909B448A5A66536000675&btag=e00008000",
|
||||
"music": "https://sf16-ies-music.tiktokcdn.com/obj/ies-music-aiso/7195358744048438042.mp3",
|
||||
"music_info": {
|
||||
"id": "7195358701144902426",
|
||||
"title": "original sound - ferryops_",
|
||||
"play": "https://sf16-ies-music.tiktokcdn.com/obj/ies-music-aiso/7195358744048438042.mp3",
|
||||
"cover": "https://p16-sign-useast2a.tiktokcdn.com/tos-useast2a-avt-0068-giso/5b59cd09101f0a570efe722f5f1f64b1~c5_1080x1080.jpeg?x-expires=1703127600&x-signature=w1qF6e5dmhhCgWPwiDvSJxZVQO4%3D",
|
||||
"author": "ferry",
|
||||
"original": true,
|
||||
"duration": 15,
|
||||
"album": ""
|
||||
},
|
||||
"play_count": 814198,
|
||||
"digg_count": 55739,
|
||||
"comment_count": 1876,
|
||||
"share_count": 1911,
|
||||
"download_count": 62,
|
||||
"create_time": 1675300014,
|
||||
"anchors": null,
|
||||
"anchors_extras": "",
|
||||
"is_ad": false,
|
||||
"commerce_info": {
|
||||
"adv_promotable": false,
|
||||
"auction_ad_invited": false,
|
||||
"branded_content_type": 0,
|
||||
"with_comment_filter_words": false
|
||||
},
|
||||
"commercial_video_info": "",
|
||||
"author": {
|
||||
"id": "6924124906000303105",
|
||||
"unique_id": "ferryops_",
|
||||
"nickname": "ferry",
|
||||
"avatar": "https://p16-sign-useast2a.tiktokcdn.com/tos-useast2a-avt-0068-giso/5b59cd09101f0a570efe722f5f1f64b1~c5_300x300.jpeg?x-expires=1703127600&x-signature=NNsWWsjrvJ206kY8qyFxiZE5k20%3D"
|
||||
},
|
||||
"is_top": 0
|
||||
},
|
||||
{
|
||||
"aweme_id": "v12044gd0000chd8vdbc77uea2obsidg",
|
||||
"video_id": "7231251211062758702",
|
||||
"region": "US",
|
||||
"title": "What would you do with this data? #computervision #machinelearning #objectdetection #csproject #softwareengineer ",
|
||||
"cover": "https://p16-sign.tiktokcdn-us.com/tos-useast5-p-0068-tx/ee1c7307d8ba46008d9a2f1002ffe99b_1683656884~tplv-dmt-logom:tos-useast5-i-0068-tx/5f1c1ee9247449aa8d86b36beeacc10d.image?x-expires=1703127600&x-signature=Wzp2xq0jwoPBoDrSEDydKP8RitY%3D&s=SEARCH&se=false&sh=&sc=dynamic_cover&l=2023122003034909B448A5A66536000675",
|
||||
"origin_cover": "https://p19-sign.tiktokcdn-us.com/tos-useast5-p-0068-tx/c5b40f31562a4b1c9bc6e9066b8da76c_1683656884~tplv-tiktokx-360p.webp?x-expires=1703127600&x-signature=q301jDESVmXKzurDSZEsAaY71u4%3D&s=SEARCH&se=false&sh=&sc=feed_cover&l=2023122003034909B448A5A66536000675",
|
||||
"duration": 126,
|
||||
"play": "https://v16m.tiktokcdn-us.com/dd179aa8851feda2a2f84f6859d626ce/6582ae74/video/tos/useast5/tos-useast5-pve-0068-tx/osaeSLPIjUoAMkRbctIQWFnIjDbeDH1RCACekt/?a=1233&ch=0&cr=13&dr=0&lr=all&cd=0%7C0%7C0%7C&cv=1&br=1490&bt=745&bti=NTY6QGo0QHM6OjZANDQuYCMucCMxNDNg&cs=0&ds=6&ft=kJrRfy7oZNx0PD1J-wtxg9w8HGM5kEeC~&mime_type=video_mp4&qs=0&rc=Ojg4OGZnPDtlZzY0Ojk6NEBpamxlNWg6Zmc7azMzZzczNEAxYV5iMy1gXjIxY2ExNi0yYSNhcGxecjRfc2FgLS1kMS9zcw%3D%3D&l=2023122003034909B448A5A66536000675&btag=e00010000",
|
||||
"wmplay": "https://v16m.tiktokcdn-us.com/b36feb41d71b86ed6e4e6ea7a72e71af/6582ae74/video/tos/useast5/tos-useast5-pve-0068-tx/oAIHCeIo1WkngCFIA1kYRbDfeIAjagtcq28oYQ/?a=1233&ch=0&cr=13&dr=0&lr=all&cd=0%7C0%7C0%7C&cv=1&br=1548&bt=774&bti=NTY6QGo0QHM6OjZANDQuYCMucCMxNDNg&cs=0&ds=3&ft=kJrRfy7oZNx0PD1J-wtxg9w8HGM5kEeC~&mime_type=video_mp4&qs=0&rc=Mzw7Zjs2NjY1MzxpNTc3aEBpamxlNWg6Zmc7azMzZzczNEBfYy40XzBeNWAxMWEuNF5gYSNhcGxecjRfc2FgLS1kMS9zcw%3D%3D&l=2023122003034909B448A5A66536000675&btag=e00010000",
|
||||
"music": "https://sf19.tiktokcdn-us.com/obj/ies-music-tx/7231251227135281962.mp3",
|
||||
"music_info": {
|
||||
"id": "7231251193572477742",
|
||||
"title": "original sound - codingai",
|
||||
"play": "https://sf19.tiktokcdn-us.com/obj/ies-music-tx/7231251227135281962.mp3",
|
||||
"cover": "https://p16-sign.tiktokcdn-us.com/tos-useast8-avt-0068-tx2/69600b7c44fc05a3b118277409c5c6fb~c5_1080x1080.jpeg?x-expires=1703127600&x-signature=eg%2B7HmT9JXLgeMm0WgzQHu9nh2I%3D",
|
||||
"author": "Eric",
|
||||
"original": true,
|
||||
"duration": 126,
|
||||
"album": ""
|
||||
},
|
||||
"play_count": 277284,
|
||||
"digg_count": 20971,
|
||||
"comment_count": 188,
|
||||
"share_count": 868,
|
||||
"download_count": 221,
|
||||
"create_time": 1683656883,
|
||||
"anchors": null,
|
||||
"anchors_extras": "",
|
||||
"is_ad": false,
|
||||
"commerce_info": {
|
||||
"adv_promotable": false,
|
||||
"auction_ad_invited": false,
|
||||
"branded_content_type": 0,
|
||||
"with_comment_filter_words": false
|
||||
},
|
||||
"commercial_video_info": "",
|
||||
"author": {
|
||||
"id": "7044388915265668143",
|
||||
"unique_id": "codingai",
|
||||
"nickname": "Eric",
|
||||
"avatar": "https://p16-sign.tiktokcdn-us.com/tos-useast8-avt-0068-tx2/69600b7c44fc05a3b118277409c5c6fb~c5_300x300.jpeg?x-expires=1703127600&x-signature=2n0cRjUEqT4lHZcZkoKH0wSURVY%3D"
|
||||
},
|
||||
"is_top": 0
|
||||
},
|
||||
{
|
||||
"aweme_id": "v09044g40000c9g4kbrc77u91sdh9jp0",
|
||||
"video_id": "7088747410289102086",
|
||||
"region": "IT",
|
||||
"title": "Come installare #ubuntu e far risorgere un vecchio pc #aletech",
|
||||
"cover": "https://p16-sign-va.tiktokcdn.com/tos-maliva-p-0068/e25e3d586f4c4cdfa18235f07b686497_1650477660~tplv-dmt-logom:tos-useast2a-v-0068/85ca53bad5e4402ba107aa9f88f3d396.image?x-expires=1703127600&x-signature=D18vkekhPxb8FvOOLcFLUcx9xLA%3D&s=SEARCH&se=false&sh=&sc=dynamic_cover&l=2023122003034909B448A5A66536000675",
|
||||
"origin_cover": "https://p16-sign-va.tiktokcdn.com/tos-maliva-p-0068/41390a6f86074b92b6f2c0e0f7862940_1650477670~tplv-tiktokx-360p.webp?x-expires=1703127600&x-signature=jTNGQhpF1J2xAkQt9Y99KyBUK%2Fs%3D&s=SEARCH&se=false&sh=&sc=feed_cover&l=2023122003034909B448A5A66536000675",
|
||||
"duration": 42,
|
||||
"play": "https://v16m.tiktokcdn-us.com/2911fa4f722b77bde13d550ed592bd72/6582ae20/video/tos/useast2a/tos-useast2a-ve-0068c003/386472d4c6fa4b479b2cf07c829bf268/?a=1233&ch=0&cr=13&dr=0&lr=all&cd=0%7C0%7C0%7C&cv=1&br=3076&bt=1538&bti=NTY6QGo0QHM6OjZANDQuYCMucCMxNDNg&cs=0&ds=6&ft=kJrRfy7oZNx0PD1J-wtxg9w8HGM5kEeC~&mime_type=video_mp4&qs=0&rc=NDVlPDc0ZDNkZjo7ZGRnaUBpM21rdjw6ZmU3PDMzNzczM0AwMF5jNi0yNV4xNDJiXzAyYSNtNmEucjRvaGRgLS1kMTZzcw%3D%3D&l=2023122003034909B448A5A66536000675&btag=e00008000",
|
||||
"wmplay": "https://v16m.tiktokcdn-us.com/9cc6a5d30e0c8948fa294f0027bd0c53/6582ae20/video/tos/useast2a/tos-useast2a-ve-0068c003/9262ec5e631d49e399d3a4bec28db2a7/?a=1233&ch=0&cr=13&dr=0&lr=all&cd=0%7C0%7C0%7C&cv=1&br=3068&bt=1534&bti=NTY6QGo0QHM6OjZANDQuYCMucCMxNDNg&cs=0&ds=3&ft=kJrRfy7oZNx0PD1J-wtxg9w8HGM5kEeC~&mime_type=video_mp4&qs=0&rc=NTxmOmVoO2g0aDk3ZzlkZkBpM21rdjw6ZmU3PDMzNzczM0A0NDU1XzUuNi4xX15gYDQuYSNtNmEucjRvaGRgLS1kMTZzcw%3D%3D&l=2023122003034909B448A5A66536000675&btag=e00008000",
|
||||
"music": "https://sf16-ies-music-sg.tiktokcdn.com/obj/tos-alisg-ve-2774/38acb76cefbb49a187e366d26cf62dcc",
|
||||
"music_info": {
|
||||
"id": "6756231813640751105",
|
||||
"title": "Better Days",
|
||||
"play": "https://sf16-ies-music-sg.tiktokcdn.com/obj/tos-alisg-ve-2774/38acb76cefbb49a187e366d26cf62dcc",
|
||||
"cover": "https://p16-sg.tiktokcdn.com/aweme/720x720/tos-alisg-v-2774/f2bad9596ac24f3a891c8fd1d5f1ac14.jpeg",
|
||||
"author": "",
|
||||
"original": false,
|
||||
"duration": 60,
|
||||
"album": "Better Days"
|
||||
},
|
||||
"play_count": 96398,
|
||||
"digg_count": 6714,
|
||||
"comment_count": 221,
|
||||
"share_count": 378,
|
||||
"download_count": 771,
|
||||
"create_time": 1650477624,
|
||||
"anchors": null,
|
||||
"anchors_extras": "",
|
||||
"is_ad": false,
|
||||
"commerce_info": {
|
||||
"adv_promotable": false,
|
||||
"auction_ad_invited": false,
|
||||
"branded_content_type": 0,
|
||||
"with_comment_filter_words": false
|
||||
},
|
||||
"commercial_video_info": "",
|
||||
"author": {
|
||||
"id": "6903482507896587266",
|
||||
"unique_id": "ale_tech_",
|
||||
"nickname": "Ale_tech",
|
||||
"avatar": "https://p16-sign-sg.tiktokcdn.com/tos-alisg-avt-0068/81f0a65e38808852ad043323d2ce9f1f~c5_300x300.jpeg?x-expires=1703127600&x-signature=djzLzF0pH%2BAV9a8qJvTZh8WNYJE%3D"
|
||||
},
|
||||
"is_top": 0
|
||||
},
|
||||
{
|
||||
"aweme_id": "v07025g50000c1ld595r2810d6r02sr0",
|
||||
"video_id": "6947597057498680578",
|
||||
"region": "ID",
|
||||
"title": "Reply to @efryday1 segini aja ya, klo full sampe instalasi ga cukup \ud83d\ude01 #linux #ubuntu #fyp",
|
||||
"cover": "https://p16-sign-sg.tiktokcdn.com/tos-alisg-p-0037/64ad02f4cfed4bd6961c1c1c992f7df1_1617613505~tplv-dmt-logom:tos-alisg-i-0000/d375b8ca573c48e18f7f19175091a0a2.image?x-expires=1703127600&x-signature=GcUS%2Bet8IeNtkQKFSCTILJMULwU%3D&s=SEARCH&se=false&sh=&sc=dynamic_cover&l=2023122003034909B448A5A66536000675",
|
||||
"origin_cover": "https://p16-sign-sg.tiktokcdn.com/tos-alisg-p-0037/f3fb7b0dc7aa4337bcfd1badd15b403e_1617613505~tplv-tiktokx-360p.webp?x-expires=1703127600&x-signature=Zea7B3dtoIkndr3j5gE0ehRiM6g%3D&s=SEARCH&se=false&sh=&sc=feed_cover&l=2023122003034909B448A5A66536000675",
|
||||
"duration": 31,
|
||||
"play": "https://v16m.tiktokcdn-us.com/a46680841f4e969f7c489dd568c85b63/6582ae15/video/tos/alisg/tos-alisg-pve-0037/efc3dc88570e4b408f460da5b30fb8cf/?a=1233&ch=0&cr=13&dr=0&lr=all&cd=0%7C0%7C0%7C&cv=1&br=2154&bt=1077&bti=NEBzNTY6QGo6OjZALnAjNDQuYCMxNDNg&cs=0&ds=6&ft=kJrRfy7oZNx0PD1J-wtxg9w8HGM5kEeC~&mime_type=video_mp4&qs=0&rc=N2lpaGg2ODpmZDM7aGZkOkBpM3YzOTM7dTxnNDMzODgzM0A0Yl9fNDQvXl8xLTEzMS4vYSNvL29hLi8yMmlgLS1kLzRzcw%3D%3D&l=2023122003034909B448A5A66536000675&btag=e00008000",
|
||||
"wmplay": "https://v16m.tiktokcdn-us.com/6ec40d75d2efc39bbbc62d727e37dcb5/6582ae15/video/tos/alisg/tos-alisg-pve-0037/a773953f390642158983d23034e27b84/?a=1233&ch=0&cr=13&dr=0&lr=all&cd=0%7C0%7C0%7C&cv=1&br=2146&bt=1073&bti=NEBzNTY6QGo6OjZALnAjNDQuYCMxNDNg&cs=0&ds=3&ft=kJrRfy7oZNx0PD1J-wtxg9w8HGM5kEeC~&mime_type=video_mp4&qs=0&rc=MzNnOGk2PDMzaDQ2Nzg8ZUBpM3YzOTM7dTxnNDMzODgzM0AuMTIxMF42Xl4xYl8tYV9fYSNvL29hLi8yMmlgLS1kLzRzcw%3D%3D&l=2023122003034909B448A5A66536000675&btag=e00008000",
|
||||
"music": "https://sf16-ies-music-sg.tiktokcdn.com/obj/tos-alisg-ve-2774/o4DXDrZtQBCbzoc9djRgk49nQBelNUnnqBesLO",
|
||||
"music_info": {
|
||||
"id": "6936177291449862145",
|
||||
"title": "Terpesona",
|
||||
"play": "https://sf16-ies-music-sg.tiktokcdn.com/obj/tos-alisg-ve-2774/o4DXDrZtQBCbzoc9djRgk49nQBelNUnnqBesLO",
|
||||
"cover": "https://p16-sg.tiktokcdn.com/aweme/720x720/tos-alisg-v-2774/3c59e86071a6488386f0eb8d0520c80e.jpeg",
|
||||
"author": "",
|
||||
"original": false,
|
||||
"duration": 60,
|
||||
"album": "Terpesona"
|
||||
},
|
||||
"play_count": 90199,
|
||||
"digg_count": 5316,
|
||||
"comment_count": 138,
|
||||
"share_count": 56,
|
||||
"download_count": 329,
|
||||
"create_time": 1617613504,
|
||||
"anchors": null,
|
||||
"anchors_extras": "",
|
||||
"is_ad": false,
|
||||
"commerce_info": {
|
||||
"adv_promotable": false,
|
||||
"auction_ad_invited": false,
|
||||
"branded_content_type": 0,
|
||||
"with_comment_filter_words": false
|
||||
},
|
||||
"commercial_video_info": "",
|
||||
"author": {
|
||||
"id": "6568018206484742146",
|
||||
"unique_id": "nvmbrxi",
|
||||
"nickname": "adit, iya pasaran iya",
|
||||
"avatar": "https://p16-sign-sg.tiktokcdn.com/tos-alisg-avt-0068/468629c8128b024d5787e670f9b42cf5~c5_300x300.jpeg?x-expires=1703127600&x-signature=t7lOKDBIC5r4n%2BalT1G8j4jKHGQ%3D"
|
||||
},
|
||||
"is_top": 0
|
||||
},
|
||||
{
|
||||
"aweme_id": "v09044g40000ciktif3c77ub4lij07ag",
|
||||
"video_id": "7253570500482321669",
|
||||
"region": "DO",
|
||||
"title": "If you love coding leave your \u2764\ufe0f #softwareengineer #softwaredeveloper #focus #fullstackdeveloper #ubuntudesktop #ubuntu #cybersecurity #reactnativedeveloper #androiddev #androiddeveloper #frontenddeveloper credits: mobiledevpro \ud83d\ude4f",
|
||||
"cover": "https://p16-sign-va.tiktokcdn.com/obj/tos-maliva-p-0068/5290f82af0a041f4ae3482161633c5ec_1688853494?x-expires=1703127600&x-signature=Qp1zu10vuLK%2BNq%2FcoElwf7hDO%2Bo%3D&s=SEARCH&se=false&sh=&sc=dynamic_cover&l=2023122003034909B448A5A66536000675",
|
||||
"origin_cover": "https://p16-sign-va.tiktokcdn.com/tos-maliva-p-0068/4c4a0d1a522c47e9bdd2b4e033cba110_1688853493~tplv-tiktokx-360p.webp?x-expires=1703127600&x-signature=zy%2BADnksas488G1HYiROxwdXsKo%3D&s=SEARCH&se=false&sh=&sc=feed_cover&l=2023122003034909B448A5A66536000675",
|
||||
"duration": 11,
|
||||
"play": "https://v16m.tiktokcdn-us.com/84404bbc3b0d32ee0baea347f3a1d0ef/6582ae01/video/tos/useast2a/tos-useast2a-ve-0068c001/ocfAhU8g7ZJ81dITBenAQkqsRlkQtB9EPdDTbX/?a=1233&ch=0&cr=13&dr=0&lr=all&cd=0%7C0%7C0%7C&cv=1&br=1368&bt=684&bti=NEBzNTY6QGo6OjZALnAjNDQuYCMxNDNg&cs=0&ds=6&ft=kJrRfy7oZNx0PD1J-wtxg9w8HGM5kEeC~&mime_type=video_mp4&qs=0&rc=NjlpOzxlMzhoOjgzOThpZEBpajptb2U6Zml3bDMzNzczM0BiYC9jYjMuNjIxM2IwMy4yYSNeLWYxcjQwZmhgLS1kMTZzcw%3D%3D&l=2023122003034909B448A5A66536000675&btag=e00008000",
|
||||
"wmplay": "https://v16m.tiktokcdn-us.com/a2d5770f1c2b3957e6394ba7426f6d67/6582ae01/video/tos/maliva/tos-maliva-ve-0068c801-us/o4JUKZ6bhM7pCAKIC4rnZCAeej5fUS2y1gmPJH/?a=1233&ch=0&cr=13&dr=0&lr=all&cd=0%7C0%7C0%7C&cv=1&br=1418&bt=709&bti=NEBzNTY6QGo6OjZALnAjNDQuYCMxNDNg&cs=0&ds=3&ft=kJrRfy7oZNx0PD1J-wtxg9w8HGM5kEeC~&mime_type=video_mp4&qs=0&rc=NTw4Ojk2NWlkODtkaGU4ZEBpajptb2U6Zml3bDMzNzczM0BjYjZgXi5hXzIxMC9fL2AuYSNeLWYxcjQwZmhgLS1kMTZzcw%3D%3D&l=2023122003034909B448A5A66536000675&btag=e00008000",
|
||||
"music": "https://sf16-ies-music.tiktokcdn.com/obj/ies-music-aiso/7232735770502859546.mp3",
|
||||
"music_info": {
|
||||
"id": "7232735763421596442",
|
||||
"title": "All my Life Lil Durk DrillyEditz",
|
||||
"play": "https://sf16-ies-music.tiktokcdn.com/obj/ies-music-aiso/7232735770502859546.mp3",
|
||||
"cover": "https://p16-sign-useast2a.tiktokcdn.com/tos-useast2a-avt-0068-euttp/b05fa4d33296c39c75fbc760a3f3ec5c~c5_1080x1080.jpeg?x-expires=1703127600&x-signature=8MeRalgtFOdb9QfxLhB2QcnIToo%3D",
|
||||
"author": "DrillyEditz\ud83e\udd77\ud83c\udffd",
|
||||
"original": true,
|
||||
"duration": 16,
|
||||
"album": ""
|
||||
},
|
||||
"play_count": 35581,
|
||||
"digg_count": 2212,
|
||||
"comment_count": 76,
|
||||
"share_count": 26,
|
||||
"download_count": 3,
|
||||
"create_time": 1688853492,
|
||||
"anchors": null,
|
||||
"anchors_extras": "",
|
||||
"is_ad": false,
|
||||
"commerce_info": {
|
||||
"adv_promotable": false,
|
||||
"auction_ad_invited": false,
|
||||
"branded_content_type": 0,
|
||||
"with_comment_filter_words": false
|
||||
},
|
||||
"commercial_video_info": "",
|
||||
"author": {
|
||||
"id": "7150316965491721221",
|
||||
"unique_id": "systemf4iled",
|
||||
"nickname": "System F4iled",
|
||||
"avatar": "https://p16-sign-va.tiktokcdn.com/tos-maliva-avt-0068/742fa96d7edbfd240757b83abf1dee0b~c5_300x300.jpeg?x-expires=1703127600&x-signature=q8rwCWfCZfWurxF6cfYOaSFciNo%3D"
|
||||
},
|
||||
"is_top": 0
|
||||
},
|
||||
{
|
||||
"aweme_id": "v09044g40000cbsv2tjc77uftkprfpdg",
|
||||
"video_id": "7131996966136958213",
|
||||
"region": "TH",
|
||||
"title": "Linux > Windows as always #Windows #Windows10 #Linux #Ubuntu #UbuntuMATE #computer #fyp",
|
||||
"cover": "https://p16-sign-va.tiktokcdn.com/tos-maliva-p-0068/421b9598296b4bc6923c82eeb0f74104_1660547458~tplv-dmt-logom:tos-useast2a-v-0068/ab4d5c65921f4556be5bd4218a05e684.image?x-expires=1703127600&x-signature=mKiZ6hytGR6zx7ek4ORgtryE6w8%3D&s=SEARCH&se=false&sh=&sc=dynamic_cover&l=2023122003034909B448A5A66536000675",
|
||||
"origin_cover": "https://p16-sign-va.tiktokcdn.com/tos-maliva-p-0068/e8f1878fc8ba466aa5af013169e33e84_1660547459~tplv-tiktokx-360p.webp?x-expires=1703127600&x-signature=6L2yZY%2FL03UOop%2FV7q7SX1Q64hI%3D&s=SEARCH&se=false&sh=&sc=feed_cover&l=2023122003034909B448A5A66536000675",
|
||||
"duration": 15,
|
||||
"play": "https://v19.tiktokcdn-us.com/edc39bdef7b4266e334ae233f724aeb6/6582ae05/video/tos/useast2a/tos-useast2a-pve-0068/4d8f2b0e1f2b4f60ad84a60fa124bfc3/?a=1233&ch=0&cr=13&dr=0&lr=all&cd=0%7C0%7C0%7C&cv=1&br=3178&bt=1589&bti=NTY6QGo0QHM6OjZANDQuYCMucCMxNDNg&cs=0&ds=6&ft=kJrRfy7oZNx0PD1J-wtxg9w8HGM5kEeC~&mime_type=video_mp4&qs=0&rc=NWk6O2hkaDdkM2g7ZTg6PEBpanN1bmk6Znd5ZTMzNzczM0AyYl9eLTReXzYxXzYvLTI0YSNhY21xcjRnL3BgLS1kMTZzcw%3D%3D&l=2023122003034909B448A5A66536000675&btag=e00008000",
|
||||
"wmplay": "https://v19.tiktokcdn-us.com/c882763d2ef55a77003b1dace8fd7fd1/6582ae05/video/tos/maliva/tos-maliva-ve-0068c801-us/512ba1d0e8824c9087d6aea78ffa3a6b/?a=1233&ch=0&cr=13&dr=0&lr=all&cd=0%7C0%7C0%7C&cv=1&br=3388&bt=1694&bti=NTY6QGo0QHM6OjZANDQuYCMucCMxNDNg&cs=0&ds=3&ft=kJrRfy7oZNx0PD1J-wtxg9w8HGM5kEeC~&mime_type=video_mp4&qs=0&rc=Ojc5NGk7aTdnM2RkOjQ2ZkBpanN1bmk6Znd5ZTMzNzczM0AtXmJhYC5fXy0xXzItYzRhYSNhY21xcjRnL3BgLS1kMTZzcw%3D%3D&l=2023122003034909B448A5A66536000675&btag=e00008000",
|
||||
"music": "https://sf16-ies-music-sg.tiktokcdn.com/obj/tos-alisg-ve-2774/ocvaqFeexInKSUDFFPAAbfDlG4UPzTODg5VBEC",
|
||||
"music_info": {
|
||||
"id": "6915348665175526145",
|
||||
"title": "Can You Feel My Heart",
|
||||
"play": "https://sf16-ies-music-sg.tiktokcdn.com/obj/tos-alisg-ve-2774/ocvaqFeexInKSUDFFPAAbfDlG4UPzTODg5VBEC",
|
||||
"cover": "https://p16-sg.tiktokcdn.com/aweme/720x720/tos-alisg-v-2774/b7c28602318840fbbb4f4b0f2ed7ddf9.jpeg",
|
||||
"author": "",
|
||||
"original": false,
|
||||
"duration": 15,
|
||||
"album": "Can You Feel My Heart"
|
||||
},
|
||||
"play_count": 36974,
|
||||
"digg_count": 1078,
|
||||
"comment_count": 202,
|
||||
"share_count": 23,
|
||||
"download_count": 3,
|
||||
"create_time": 1660547455,
|
||||
"anchors": null,
|
||||
"anchors_extras": "",
|
||||
"is_ad": false,
|
||||
"commerce_info": {
|
||||
"adv_promotable": false,
|
||||
"auction_ad_invited": false,
|
||||
"branded_content_type": 0,
|
||||
"with_comment_filter_words": false
|
||||
},
|
||||
"commercial_video_info": "",
|
||||
"author": {
|
||||
"id": "6988352212003243009",
|
||||
"unique_id": "hannor_smith",
|
||||
"nickname": "han.flac \ud83c\udf1f\ud83c\udfa7\ud83c\udfcd\ufe0f",
|
||||
"avatar": "https://p16-sign-va.tiktokcdn.com/tos-maliva-avt-0068/58167255ee1d05d70dffc59ec0286e96~c5_300x300.jpeg?x-expires=1703127600&x-signature=N3KkHMBaqDBqVcHCt833OonyJ58%3D"
|
||||
},
|
||||
"is_top": 0
|
||||
},
|
||||
{
|
||||
"aweme_id": "v12044gd0000cb4rr83c77u2coev35og",
|
||||
"video_id": "7118429570973519150",
|
||||
"region": "US",
|
||||
"title": "If you haven't tried Linux yet, Ubuntu should be your first \ud83e\udd79\ud83d\udcbb #linuxdistro #ubuntulinux #oldlaptopmotherboard #oldpchardware #linux",
|
||||
"cover": "https://p19-sign.tiktokcdn-us.com/obj/tos-useast5-p-0068-tx/57a059b3776f4deeaa4a7297dd9d46fa_1657388545?x-expires=1703127600&x-signature=2HaAe082pCt55gBKb9YvXCyITyw%3D&s=SEARCH&se=false&sh=&sc=dynamic_cover&l=2023122003034909B448A5A66536000675",
|
||||
"origin_cover": "https://p16-sign.tiktokcdn-us.com/tos-useast5-p-0068-tx/8680ac0cc03840df9ff6f003cbeb74f6_1657388545~tplv-tiktokx-360p.webp?x-expires=1703127600&x-signature=s3Xz2qsI2um65ZUbw3Lg8SQYo20%3D&s=SEARCH&se=false&sh=&sc=feed_cover&l=2023122003034909B448A5A66536000675",
|
||||
"duration": 30,
|
||||
"play": "https://v19.tiktokcdn-us.com/607a59a261b518e59c26873e2d17f22d/6582ae13/video/tos/useast5/tos-useast5-pve-0068-tx/948fbacccb394ae2b722a1e39f3daa59/?a=1233&ch=0&cr=13&dr=0&lr=all&cd=0%7C0%7C0%7C&cv=1&br=1524&bt=762&bti=NTY6QGo0QHM6OjZANDQuYCMucCMxNDNg&cs=0&ds=6&ft=kJrRfy7oZNx0PD1J-wtxg9w8HGM5kEeC~&mime_type=video_mp4&qs=0&rc=aDY4PDw5OjM1ZWlkNDxpaUBpajh5cjU6Zjt1ZTMzZzczNEAzXi4tYl4uNi0xLjBeNC5hYSNsMGJgcjQwbzFgLS1kMS9zcw%3D%3D&l=2023122003034909B448A5A66536000675&btag=e00008000",
|
||||
"wmplay": "https://v19.tiktokcdn-us.com/eee0d5c470e85e78957dc78605caf792/6582ae13/video/tos/useast5/tos-useast5-ve-0068c001-tx/27add3d74a14413091513d4c7a3bd553/?a=1233&ch=0&cr=13&dr=0&lr=all&cd=0%7C0%7C0%7C&cv=1&br=1522&bt=761&bti=NTY6QGo0QHM6OjZANDQuYCMucCMxNDNg&cs=0&ds=3&ft=kJrRfy7oZNx0PD1J-wtxg9w8HGM5kEeC~&mime_type=video_mp4&qs=0&rc=aDppOTdmPDY2OzszaDtnOkBpajh5cjU6Zjt1ZTMzZzczNEBgL2IzNDFhNWAxNWFgYTU1YSNsMGJgcjQwbzFgLS1kMS9zcw%3D%3D&l=2023122003034909B448A5A66536000675&btag=e00008000",
|
||||
"music": "https://sf16-ies-music-sg.tiktokcdn.com/obj/tos-alisg-ve-2774/oc1hI8e1jwQ9aPyoBOnCUc6YZwjgZBbt1GDfbY",
|
||||
"music_info": {
|
||||
"id": "6778968637492430849",
|
||||
"title": "Blue Blood",
|
||||
"play": "https://sf16-ies-music-sg.tiktokcdn.com/obj/tos-alisg-ve-2774/oc1hI8e1jwQ9aPyoBOnCUc6YZwjgZBbt1GDfbY",
|
||||
"cover": "https://p16-sg.tiktokcdn.com/aweme/720x720/tos-alisg-v-2774/c24e133f8d134031b03a673aa747ed6d.jpeg",
|
||||
"author": "",
|
||||
"original": false,
|
||||
"duration": 60,
|
||||
"album": "It's Always Sunny In Philadelphia (Music from the Original FX Series)"
|
||||
},
|
||||
"play_count": 30464,
|
||||
"digg_count": 817,
|
||||
"comment_count": 29,
|
||||
"share_count": 34,
|
||||
"download_count": 84,
|
||||
"create_time": 1657388542,
|
||||
"anchors": null,
|
||||
"anchors_extras": "",
|
||||
"is_ad": false,
|
||||
"commerce_info": {
|
||||
"adv_promotable": false,
|
||||
"auction_ad_invited": false,
|
||||
"branded_content_type": 0,
|
||||
"with_comment_filter_words": false
|
||||
},
|
||||
"commercial_video_info": "",
|
||||
"author": {
|
||||
"id": "7113859000824316974",
|
||||
"unique_id": "buggintech",
|
||||
"nickname": "buggintech",
|
||||
"avatar": "https://p16-sign.tiktokcdn-us.com/tos-useast5-avt-0068-tx/e390a937f6bc4281817b391361497762~c5_300x300.jpeg?x-expires=1703127600&x-signature=nyeyvxtxVn%2F0SE%2Fn7Wnavk3wGjE%3D"
|
||||
},
|
||||
"is_top": 0
|
||||
},
|
||||
{
|
||||
"aweme_id": "v12044gd0000cgi7l53c77u11maehifg",
|
||||
"video_id": "7216028231185616171",
|
||||
"region": "US",
|
||||
"title": "Replying to @fltlnsb I say all this with extreme confidence, as I\u2019ve spent the last 12 years supporting users ages 30-70. People that grew up with computers around, some who have used computers for 20 years. Telling someone \u2018you need to learn an entire new platform and convert your PDF\u2019s with command line because it\u2019s better\u2019 is ridiculous. Can YOU do it faster with CLI? Yes. Does that mean everybody should? Absolutely not. Here in 2023, the vast majority of people in the work force and ones who own business are barely getting by with the Windows computers they have. Microsoft has a chokehold on the end user facing side of business/corporate/enterprise business. Uprooting their infrastructure to implement a product that will change everything they do from a day to day because \u2018it\u2019s better\u2019 - sorry, not a business expense anyone is willing to swallow. You can spend 15 hours a week for a few weeks learning the OS and figuring out whats good about it. Other people do not. Time is money. and is business, just because you csn - doesnt mean you should. #ubuntu #linux #computers #naaackers ",
|
||||
"cover": "https://p16-sign.tiktokcdn-us.com/tos-useast5-p-0068-tx/0485fb4c1f2549c8987ba323ed26993f_1680112511~tplv-dmt-logom:tos-useast5-i-0068-tx/798594786eff41458cbbc4f6da8a1476.image?x-expires=1703127600&x-signature=ZARZhRKsQwA%2FgqGM4p6Y%2BD%2F%2FRpI%3D&s=SEARCH&se=false&sh=&sc=dynamic_cover&l=2023122003034909B448A5A66536000675",
|
||||
"origin_cover": "https://p16-sign.tiktokcdn-us.com/tos-useast5-p-0068-tx/aff34ca418e1453390f96584faae5535_1680112510~tplv-tiktokx-360p.webp?x-expires=1703127600&x-signature=qiKmXCjMnnE%2B8P68jaEydOsTtEU%3D&s=SEARCH&se=false&sh=&sc=feed_cover&l=2023122003034909B448A5A66536000675",
|
||||
"duration": 58,
|
||||
"play": "https://v19.tiktokcdn-us.com/ac39a5443137abb7743118c8e6c38046/6582ae30/video/tos/useast5/tos-useast5-ve-0068c002-tx/126f8a804a18420cb9f9a0362aa3ac6c/?a=1233&ch=0&cr=13&dr=0&lr=all&cd=0%7C0%7C0%7C&cv=1&br=1760&bt=880&bti=NTY6QGo0QHM6OjZANDQuYCMucCMxNDNg&cs=0&ds=6&ft=kJrRfy7oZNx0PD1J-wtxg9w8HGM5kEeC~&mime_type=video_mp4&qs=0&rc=Mzk6NGc7Zzg3NDs2O2Q4OkBpamxocDQ6Zjg6ajMzZzczNEAxNC8wMC1hNmAxLjYwXzI2YSNjZV4ucjQwaWZgLS1kMS9zcw%3D%3D&l=2023122003034909B448A5A66536000675&btag=e00008000",
|
||||
"wmplay": "https://v19.tiktokcdn-us.com/d9bb3ef4e9ef24d612514d12c451b6cc/6582ae30/video/tos/useast5/tos-useast5-ve-0068c004-tx/f40dd86127a2452fa9b021f295880c2c/?a=1233&ch=0&cr=13&dr=0&lr=all&cd=0%7C0%7C0%7C&cv=1&br=1862&bt=931&bti=NTY6QGo0QHM6OjZANDQuYCMucCMxNDNg&cs=0&ds=3&ft=kJrRfy7oZNx0PD1J-wtxg9w8HGM5kEeC~&mime_type=video_mp4&qs=0&rc=O2g0aTM1Zjw1NWk7OTZkaEBpamxocDQ6Zjg6ajMzZzczNEBeNTJjNWJfXl4xNDZeYTBfYSNjZV4ucjQwaWZgLS1kMS9zcw%3D%3D&l=2023122003034909B448A5A66536000675&btag=e00008000",
|
||||
"music": "https://sf16.tiktokcdn-us.com/obj/ies-music-tx/7216028260373662507.mp3",
|
||||
"music_info": {
|
||||
"id": "7216028213967997739",
|
||||
"title": "original sound - naaackers",
|
||||
"play": "https://sf16.tiktokcdn-us.com/obj/ies-music-tx/7216028260373662507.mp3",
|
||||
"cover": "https://p19-sign.tiktokcdn-us.com/tos-useast5-avt-0068-tx/99794bd713d80b1c2b65aff94b0e6500~c5_1080x1080.jpeg?x-expires=1703127600&x-signature=a6w1ZerLtyqMcKa2H0ioqXBy%2FxQ%3D",
|
||||
"author": "Naaackers",
|
||||
"original": true,
|
||||
"duration": 58,
|
||||
"album": ""
|
||||
},
|
||||
"play_count": 7621,
|
||||
"digg_count": 528,
|
||||
"comment_count": 227,
|
||||
"share_count": 5,
|
||||
"download_count": 2,
|
||||
"create_time": 1680112522,
|
||||
"anchors": [
|
||||
{
|
||||
"actions": [
|
||||
{
|
||||
"action_type": 1,
|
||||
"icon": {
|
||||
"height": 720,
|
||||
"uri": "tiktok-obj/28px_primary_create_onDark3x.png",
|
||||
"url_list": [
|
||||
"https://p16-sg.tiktokcdn.com/obj/tiktok-obj/28px_primary_create_onDark3x.png?biz_tag=anchor.effect"
|
||||
],
|
||||
"url_prefix": null,
|
||||
"width": 720
|
||||
},
|
||||
"schema": "shoot"
|
||||
}
|
||||
],
|
||||
"component_key": "anchor_effect",
|
||||
"description": "Effects",
|
||||
"extra": "{\"effect_source\":0,\"is_commerce\":0}",
|
||||
"icon": {
|
||||
"height": 720,
|
||||
"uri": "tiktok-obj/20px_anchor_effect3x.png",
|
||||
"url_list": [
|
||||
"https://p16-sg.tiktokcdn.com/obj/tiktok-obj/20px_anchor_effect3x.png?biz_tag=anchor.effect"
|
||||
],
|
||||
"url_prefix": null,
|
||||
"width": 720
|
||||
},
|
||||
"id": "454747",
|
||||
"keyword": "Green Screen Video",
|
||||
"log_extra": "{\"anchor_id\":\"454747\",\"anchor_name\":\"Green Screen Video\",\"anchor_type\":\"prop\"}",
|
||||
"thumbnail": {
|
||||
"height": 64,
|
||||
"uri": "d846c3603d36559c76691965ba808340",
|
||||
"url_list": [
|
||||
"https://lf16-effectcdn-va.tiktokcdn.com/obj/ies-fe-effect-va/d846c3603d36559c76691965ba808340",
|
||||
"https://lf21-effectcdn-va.tiktokcdn.com/obj/ies-fe-effect-va/d846c3603d36559c76691965ba808340",
|
||||
"https://lf19-effectcdn-va.tiktokcdn.com/obj/ies-fe-effect-va/d846c3603d36559c76691965ba808340"
|
||||
],
|
||||
"url_prefix": null,
|
||||
"width": 64
|
||||
},
|
||||
"type": 28
|
||||
}
|
||||
],
|
||||
"anchors_extras": "",
|
||||
"is_ad": false,
|
||||
"commerce_info": {
|
||||
"adv_promotable": false,
|
||||
"auction_ad_invited": false,
|
||||
"branded_content_type": 0,
|
||||
"with_comment_filter_words": false
|
||||
},
|
||||
"commercial_video_info": "",
|
||||
"author": {
|
||||
"id": "6757790857447539718",
|
||||
"unique_id": "naaackers",
|
||||
"nickname": "Naaackers",
|
||||
"avatar": "https://p19-sign.tiktokcdn-us.com/tos-useast5-avt-0068-tx/99794bd713d80b1c2b65aff94b0e6500~c5_300x300.jpeg?x-expires=1703127600&x-signature=f7mkopk%2F2EG%2FpA98qcgATY8nj80%3D"
|
||||
},
|
||||
"is_top": 0
|
||||
},
|
||||
{
|
||||
"aweme_id": "v0f025gc0000cfaidr3c77u090l3co8g",
|
||||
"video_id": "7193698776148725019",
|
||||
"region": "TH",
|
||||
"title": "Life of a Linux user \ud83d\udc27 #linux #ubuntu #computer #laptop #tech #techtok ",
|
||||
"cover": "https://p16-sign-useast2a.tiktokcdn.com/obj/tos-useast2a-p-0037-aiso/6fb0705918f0413b9127aa0c2c95c0ed_1674913526?x-expires=1703127600&x-signature=yNngPeJxRl5AB6rIiA3EjO%2BO6Gg%3D&s=SEARCH&se=false&sh=&sc=dynamic_cover&l=2023122003034909B448A5A66536000675",
|
||||
"origin_cover": "https://p16-sign-useast2a.tiktokcdn.com/tos-useast2a-p-0037-aiso/908c47399ad2445b8720a5c0c49e45fa_1674913529~tplv-tiktokx-360p.webp?x-expires=1703127600&x-signature=yt9L2hrWPZ3Z1mbX9tVT8KDOeB0%3D&s=SEARCH&se=false&sh=&sc=feed_cover&l=2023122003034909B448A5A66536000675",
|
||||
"duration": 15,
|
||||
"play": "https://v19.tiktokcdn-us.com/3e418870739bcd9b1a1244bfa89d4b8f/6582ae05/video/tos/useast2a/tos-useast2a-pve-0037-aiso/oMRKA6h7oCSVjVnvBCN2JToIzNA9fhQY2AIxpU/?a=1233&ch=0&cr=13&dr=0&lr=all&cd=0%7C0%7C0%7C&cv=1&br=2328&bt=1164&bti=NTY6QGo0QHM6OjZANDQuYCMucCMxNDNg&cs=0&ds=6&ft=kJrRfy7oZNx0PD1J-wtxg9w8HGM5kEeC~&mime_type=video_mp4&qs=0&rc=aGYzNDs5ZDlnNzhmOjk8NEBpanI2MzM6ZnVsaTMzZjgzM0AvMDM0LzQxXjExMGFhNTIyYSM1YGk2cjQwYV5gLS1kL2Nzcw%3D%3D&l=2023122003034909B448A5A66536000675&btag=e00008000",
|
||||
"wmplay": "https://v19.tiktokcdn-us.com/1e5ce0481d836bbef971d1c12ebefd60/6582ae05/video/tos/useast2a/tos-useast2a-pve-0037-aiso/o43NInhzZSNh2AoQixpVBYRUbCf2CKA6o2A6Wo/?a=1233&ch=0&cr=13&dr=0&lr=all&cd=0%7C0%7C0%7C&cv=1&br=2616&bt=1308&bti=NTY6QGo0QHM6OjZANDQuYCMucCMxNDNg&cs=0&ds=3&ft=kJrRfy7oZNx0PD1J-wtxg9w8HGM5kEeC~&mime_type=video_mp4&qs=0&rc=ZTQ1OjRlOTc5NDg5OzRmPEBpanI2MzM6ZnVsaTMzZjgzM0A2MDE2MGJhNmAxMy8uLzYtYSM1YGk2cjQwYV5gLS1kL2Nzcw%3D%3D&l=2023122003034909B448A5A66536000675&btag=e00008000",
|
||||
"music": "https://sf16-ies-music-sg.tiktokcdn.com/obj/tos-alisg-ve-2774/oYnmdQz4MBF51IDQzbZptpTBBaALqgveOLC1ek",
|
||||
"music_info": {
|
||||
"id": "6969850109240559617",
|
||||
"title": "Meow",
|
||||
"play": "https://sf16-ies-music-sg.tiktokcdn.com/obj/tos-alisg-ve-2774/oYnmdQz4MBF51IDQzbZptpTBBaALqgveOLC1ek",
|
||||
"cover": "https://p16-sg.tiktokcdn.com/aweme/720x720/tos-alisg-v-2774/df24a43905f9413bb3596cb8c765bc16.jpeg",
|
||||
"author": "",
|
||||
"original": false,
|
||||
"duration": 60,
|
||||
"album": "Meow"
|
||||
},
|
||||
"play_count": 29000,
|
||||
"digg_count": 458,
|
||||
"comment_count": 31,
|
||||
"share_count": 9,
|
||||
"download_count": 1,
|
||||
"create_time": 1674913523,
|
||||
"anchors": null,
|
||||
"anchors_extras": "",
|
||||
"is_ad": false,
|
||||
"commerce_info": {
|
||||
"adv_promotable": false,
|
||||
"auction_ad_invited": false,
|
||||
"branded_content_type": 0,
|
||||
"with_comment_filter_words": false
|
||||
},
|
||||
"commercial_video_info": "",
|
||||
"author": {
|
||||
"id": "6988352212003243009",
|
||||
"unique_id": "hannor_smith",
|
||||
"nickname": "han.flac \ud83c\udf1f\ud83c\udfa7\ud83c\udfcd\ufe0f",
|
||||
"avatar": "https://p16-sign-va.tiktokcdn.com/tos-maliva-avt-0068/58167255ee1d05d70dffc59ec0286e96~c5_300x300.jpeg?x-expires=1703127600&x-signature=N3KkHMBaqDBqVcHCt833OonyJ58%3D"
|
||||
},
|
||||
"is_top": 0
|
||||
},
|
||||
{
|
||||
"aweme_id": "v12044gd0000c9hnderc77u0gkpj1f80",
|
||||
"video_id": "7089641605862313262",
|
||||
"region": "US",
|
||||
"title": "#linux #sysadmin #systemadministrator #python #fyp\u30b7 #ubuntu #tech #computerscience #networkengineer #linux #bash",
|
||||
"cover": "https://p16-sign.tiktokcdn-us.com/obj/tos-useast5-p-0068-tx/f4004afdbfb1457fa72e54aeebbae658_1650685820?x-expires=1703127600&x-signature=baO3nkbdG73H%2FLlG%2BB%2B5E7Nff3k%3D&s=SEARCH&se=false&sh=&sc=dynamic_cover&l=2023122003034909B448A5A66536000675",
|
||||
"origin_cover": "https://p16-sign.tiktokcdn-us.com/tos-useast5-p-0068-tx/8995429c7adb447083dfa93330af88bd_1650685820~tplv-tiktokx-360p.webp?x-expires=1703127600&x-signature=CPnQVAqZ%2F2C8tS%2FmgBczNQzDnmM%3D&s=SEARCH&se=false&sh=&sc=feed_cover&l=2023122003034909B448A5A66536000675",
|
||||
"duration": 47,
|
||||
"play": "https://v19.tiktokcdn-us.com/0c2c6034ef9b8ebc826a41cd168a3bff/6582ae25/video/tos/useast5/tos-useast5-ve-0068c003-tx/e25fcce499b04eaf97928d4c70f27070/?a=1233&ch=0&cr=13&dr=0&lr=all&cd=0%7C0%7C0%7C&cv=1&br=1502&bt=751&bti=NTY6QGo0QHM6OjZANDQuYCMucCMxNDNg&cs=0&ds=6&ft=kJrRfy7oZNx0PD1J-wtxg9w8HGM5kEeC~&mime_type=video_mp4&qs=0&rc=M2loZDhkZTMzaDg1OzloOUBpM2ltbjM6ZmhxPDMzZzczNEAvYzEvMDEvXjExLy9gYDQ1YSM1Lm1kcjRvYWVgLS1kMS9zcw%3D%3D&l=2023122003034909B448A5A66536000675&btag=e00008000",
|
||||
"wmplay": "https://v19.tiktokcdn-us.com/540ebc82370825c5bb5cac7605ba9166/6582ae25/video/tos/useast5/tos-useast5-pve-0068-tx/a06e405ea9f042eebc7d7d3432a5a3d3/?a=1233&ch=0&cr=13&dr=0&lr=all&cd=0%7C0%7C0%7C&cv=1&br=1498&bt=749&bti=NTY6QGo0QHM6OjZANDQuYCMucCMxNDNg&cs=0&ds=3&ft=kJrRfy7oZNx0PD1J-wtxg9w8HGM5kEeC~&mime_type=video_mp4&qs=0&rc=aTo4N2QzaDxpaDs2OjRlOkBpM2ltbjM6ZmhxPDMzZzczNEA2MC0vMmAtXmAxMGI2XjZjYSM1Lm1kcjRvYWVgLS1kMS9zcw%3D%3D&l=2023122003034909B448A5A66536000675&btag=e00008000",
|
||||
"music": "https://sf16.tiktokcdn-us.com/obj/ies-music-tx/7089641612577409835.mp3",
|
||||
"music_info": {
|
||||
"id": "7089641579396287278",
|
||||
"title": "original sound - stevenservo",
|
||||
"play": "https://sf16.tiktokcdn-us.com/obj/ies-music-tx/7089641612577409835.mp3",
|
||||
"cover": "https://p16-sign-va.tiktokcdn.com/tos-maliva-avt-0068/62713abc4ad5f8eb52869addf6611a92~c5_1080x1080.jpeg?x-expires=1703127600&x-signature=x8ba50BVMtG%2BVOppSeHHwPsi2x8%3D",
|
||||
"author": "StevenServo",
|
||||
"original": true,
|
||||
"duration": 47,
|
||||
"album": ""
|
||||
},
|
||||
"play_count": 4083,
|
||||
"digg_count": 427,
|
||||
"comment_count": 28,
|
||||
"share_count": 18,
|
||||
"download_count": 44,
|
||||
"create_time": 1650685819,
|
||||
"anchors": null,
|
||||
"anchors_extras": "",
|
||||
"is_ad": false,
|
||||
"commerce_info": {
|
||||
"adv_promotable": false,
|
||||
"auction_ad_invited": false,
|
||||
"branded_content_type": 0,
|
||||
"with_comment_filter_words": false
|
||||
},
|
||||
"commercial_video_info": "",
|
||||
"author": {
|
||||
"id": "6941572883425166342",
|
||||
"unique_id": "stevenservo",
|
||||
"nickname": "StevenServo",
|
||||
"avatar": "https://p16-sign-va.tiktokcdn.com/tos-maliva-avt-0068/62713abc4ad5f8eb52869addf6611a92~c5_300x300.jpeg?x-expires=1703127600&x-signature=sQFsgOJXOOxGQtHf4cwmfNrSKX4%3D"
|
||||
},
|
||||
"is_top": 0
|
||||
}
|
||||
],
|
||||
"cursor": 10,
|
||||
"hasMore": true
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
import requests
|
||||
import json
|
||||
|
||||
url = "https://tiktok-download-video1.p.rapidapi.com/feedSearch"
|
||||
|
||||
querystring = {"keywords":"VS code","count":"10","cursor":"0","region":"US","publish_time":"0","sort_type":"1"}
|
||||
|
||||
headers = {
|
||||
"X-RapidAPI-Key": "YOUR_API_KEY",
|
||||
"X-RapidAPI-Host": "tiktok-download-video1.p.rapidapi.com"
|
||||
}
|
||||
|
||||
response = requests.get(url, headers=headers, params=querystring)
|
||||
|
||||
print(response.json())
|
||||
with open("./record.json", "w") as f:
|
||||
json.dump(response.json(), f)
|
||||
@@ -1,373 +0,0 @@
|
||||
{
|
||||
"code": 0,
|
||||
"msg": "success",
|
||||
"processed_time": 1.2462,
|
||||
"data": {
|
||||
"videos": [
|
||||
{
|
||||
"aweme_id": "v09044g40000cdrtb5jc77u48deft16g",
|
||||
"video_id": "7167433112283008261",
|
||||
"region": "NL",
|
||||
"title": "\u201cYou have been misinformed, as much as we\u2019ve been misinformed.\u201d #blackpeople #africancommunity #blackpower #blackunity #panafricanism #fyp #foryou #panafrican #afrikancommunity #blackconsciousness #melanin #africa #blackcommunity #unlearnandrelearn #blackexcellence #problack #knowthyself #ubuntu",
|
||||
"cover": "https://p16-sign-va.tiktokcdn.com/obj/tos-maliva-p-0068/b9093c120b9944fda3f547001d44a240_1668798075?x-expires=1703124000&x-signature=4%2BExSPK3MmTFdQCMYg72mi5HDzg%3D&s=SEARCH&se=false&sh=&sc=dynamic_cover&l=20231220021905B0B31567CD73EF000289",
|
||||
"origin_cover": "https://p16-sign-va.tiktokcdn.com/tos-maliva-p-0068/e124bf8b4c67438b901351498b4b5695_1668798074~tplv-tiktokx-360p.webp?x-expires=1703124000&x-signature=0bJVu2w0UcX9iAgRQW7h%2F%2F4OYSA%3D&s=SEARCH&se=false&sh=&sc=feed_cover&l=20231220021905B0B31567CD73EF000289",
|
||||
"duration": 40,
|
||||
"play": "https://v19.tiktokcdn-us.com/cce82f9db7d6c71f833e2b53e2abc39b/6582a3a2/video/tos/useast2a/tos-useast2a-pve-0068/okIE3ejh9BGhBnnpUbp4OJQQD71rNdIWeBWARo/?a=1233&ch=0&cr=13&dr=0&lr=all&cd=0%7C0%7C0%7C&cv=1&br=2522&bt=1261&bti=NEBzNTY6QGo6OjZALnAjNDQuYCMxNDNg&cs=0&ds=6&ft=kJrRfy7oZtc0PD1Agptxg9wp6DhXvEeC~&mime_type=video_mp4&qs=0&rc=NGQ1ZTc2aGY5ZTs3PDtoOUBpajRpZzc6Zjh3ZzMzNzczM0A1YDEwXi00Xi0xMjY0YWBgYSMzcWI1cjRnX29gLS1kMTZzcw%3D%3D&l=20231220021905B0B31567CD73EF000289&btag=e00008000",
|
||||
"wmplay": "https://v19.tiktokcdn-us.com/0201af034764a80c072969f8d1ebf395/6582a3a2/video/tos/useast2a/tos-useast2a-ve-0068c003/ooYQ9UGW3IjANcl1r8DJBedERBonhQWJnIb9eh/?a=1233&ch=0&cr=13&dr=0&lr=all&cd=0%7C0%7C0%7C&cv=1&br=2592&bt=1296&bti=NEBzNTY6QGo6OjZALnAjNDQuYCMxNDNg&cs=0&ds=3&ft=kJrRfy7oZtc0PD1Agptxg9wp6DhXvEeC~&mime_type=video_mp4&qs=0&rc=NTM2ZTZpNjZnNjo7NzxlZUBpajRpZzc6Zjh3ZzMzNzczM0AuNl9fYjYtNS8xLTRjMjIuYSMzcWI1cjRnX29gLS1kMTZzcw%3D%3D&l=20231220021905B0B31567CD73EF000289&btag=e00008000",
|
||||
"music": "https://sf16-ies-music-va.tiktokcdn.com/obj/musically-maliva-obj/7167433130218556166.mp3",
|
||||
"music_info": {
|
||||
"id": "7167433094969821958",
|
||||
"title": "original sound - afrikancommunity",
|
||||
"play": "https://sf16-ies-music-va.tiktokcdn.com/obj/musically-maliva-obj/7167433130218556166.mp3",
|
||||
"cover": "https://p16-sign-va.tiktokcdn.com/tos-maliva-avt-0068/93c33be7e299ea8cf1762b3ff08175bc~c5_1080x1080.jpeg?x-expires=1703124000&x-signature=kP1cVyhAwezOqb3NoY3PVxO9Oyo%3D",
|
||||
"author": "afrikancommunity",
|
||||
"original": true,
|
||||
"duration": 40,
|
||||
"album": ""
|
||||
},
|
||||
"play_count": 4059853,
|
||||
"digg_count": 785436,
|
||||
"comment_count": 5817,
|
||||
"share_count": 12996,
|
||||
"download_count": 11150,
|
||||
"create_time": 1668798067,
|
||||
"anchors": null,
|
||||
"anchors_extras": "",
|
||||
"is_ad": false,
|
||||
"commerce_info": {
|
||||
"adv_promotable": false,
|
||||
"auction_ad_invited": false,
|
||||
"branded_content_type": 0,
|
||||
"with_comment_filter_words": false
|
||||
},
|
||||
"commercial_video_info": "",
|
||||
"author": {
|
||||
"id": "6813642741462287365",
|
||||
"unique_id": "afrikancommunity",
|
||||
"nickname": "afrikancommunity",
|
||||
"avatar": "https://p16-sign-va.tiktokcdn.com/tos-maliva-avt-0068/93c33be7e299ea8cf1762b3ff08175bc~c5_300x300.jpeg?x-expires=1703124000&x-signature=QIJZsNfd2YOgDg8lMGEqGQeam24%3D"
|
||||
},
|
||||
"is_top": 0
|
||||
},
|
||||
{
|
||||
"aweme_id": "v0f025gc0000cglvmnrc77u4ni1u20bg",
|
||||
"video_id": "7212008888734878982",
|
||||
"region": "BW",
|
||||
"title": "S/O @Fiksology\ud83e\udef4 ka DC\ud83d\udd25@gavin.ubuntu @Jame McVernando #ubuntubandmerch #thamiubuntu #SAMA28 ",
|
||||
"cover": "https://p16-sign-useast2a.tiktokcdn.com/obj/tos-useast2a-p-0037-aiso/29910b3791a7474babf262da0477c5ee_1680604010?x-expires=1703124000&x-signature=96i6RGuz10acj1SwttTl1FwE9YQ%3D&s=SEARCH&se=false&sh=&sc=dynamic_cover&l=20231220021905B0B31567CD73EF000289",
|
||||
"origin_cover": "https://p16-sign-useast2a.tiktokcdn.com/tos-useast2a-p-0037-aiso/71a9299ab7a446d39f99542fefcf521a_1680604009~tplv-tiktokx-360p.webp?x-expires=1703124000&x-signature=s%2FD6JYmr8UF3GflvYGTwX%2FhlvVE%3D&s=SEARCH&se=false&sh=&sc=feed_cover&l=20231220021905B0B31567CD73EF000289",
|
||||
"duration": 22,
|
||||
"play": "https://v16m.tiktokcdn-us.com/fbc1b7c9d6df457b01e73329c76b5592/6582a390/video/tos/useast2a/tos-useast2a-pve-0037-aiso/oMAXWfuEQQ8FJFNSeEr89paDofng8LACfAbT3g/?a=1233&ch=0&cr=13&dr=0&lr=all&cd=0%7C0%7C0%7C&cv=1&br=1840&bt=920&bti=NEBzNTY6QGo6OjZALnAjNDQuYCMxNDNg&cs=0&ds=6&ft=kJrRfy7oZtc0PD1Agptxg9wp6DhXvEeC~&mime_type=video_mp4&qs=4&rc=M2dpNjhkZWY0PDhmaTc4Z0BpajN4bDc6ZnF5ajMzZjgzM0AvYjQvMC9fX2AxL2JhX14vYSNfLy5rcjRvamlgLS1kL2Nzcw%3D%3D&l=20231220021905B0B31567CD73EF000289&btag=e00008000",
|
||||
"wmplay": "https://v16m.tiktokcdn-us.com/18d13ec31cad5bffc05a1f795a3769aa/6582a390/video/tos/useast2a/tos-useast2a-pve-0037c001-aiso/o4AgX8Cfar83ne9MWbgEfbJSA8DINxHoAQCfQu/?a=1233&ch=0&cr=13&dr=0&lr=all&cd=0%7C0%7C0%7C&cv=1&br=4390&bt=2195&bti=NEBzNTY6QGo6OjZALnAjNDQuYCMxNDNg&cs=0&ds=3&ft=kJrRfy7oZtc0PD1Agptxg9wp6DhXvEeC~&mime_type=video_mp4&qs=0&rc=NjNkMzg8NmhkaWg7NDs6ZEBpajN4bDc6ZnF5ajMzZjgzM0BiXzBfX14zXjMxNDMxMjY1YSNfLy5rcjRvamlgLS1kL2Nzcw%3D%3D&l=20231220021905B0B31567CD73EF000289&btag=e00008000",
|
||||
"music": "https://sf16-ies-music-va.tiktokcdn.com/obj/musically-maliva-obj/7205224029014264582.mp3",
|
||||
"music_info": {
|
||||
"id": "7205224038348000005",
|
||||
"title": "original sound - basiiey_monnapula",
|
||||
"play": "https://sf16-ies-music-va.tiktokcdn.com/obj/musically-maliva-obj/7205224029014264582.mp3",
|
||||
"cover": "https://p16-sign-va.tiktokcdn.com/tos-maliva-avt-0068/ae049960ea6786673783ce66b36dc7da~c5_1080x1080.jpeg?x-expires=1703124000&x-signature=%2FnY5uAyksD41s%2Bm8ECAFLIAtXfo%3D",
|
||||
"author": "Basetsana Monnapula",
|
||||
"original": true,
|
||||
"duration": 32,
|
||||
"album": ""
|
||||
},
|
||||
"play_count": 6868872,
|
||||
"digg_count": 673843,
|
||||
"comment_count": 2832,
|
||||
"share_count": 8103,
|
||||
"download_count": 463,
|
||||
"create_time": 1679176676,
|
||||
"anchors": null,
|
||||
"anchors_extras": "",
|
||||
"is_ad": false,
|
||||
"commerce_info": {
|
||||
"adv_promotable": false,
|
||||
"auction_ad_invited": false,
|
||||
"branded_content_type": 0,
|
||||
"with_comment_filter_words": false
|
||||
},
|
||||
"commercial_video_info": "",
|
||||
"author": {
|
||||
"id": "217487781219024896",
|
||||
"unique_id": "thami.ubuntu",
|
||||
"nickname": "Thami Philemon",
|
||||
"avatar": "https://p16-sign-useast2a.tiktokcdn.com/tos-useast2a-avt-0068-giso/e546ddc5cc70ede037ac5397cd068323~c5_300x300.jpeg?x-expires=1703124000&x-signature=gFNFdUl%2BtBIHAI2sX2ANcjUvqyo%3D"
|
||||
},
|
||||
"is_top": 0
|
||||
},
|
||||
{
|
||||
"aweme_id": "v09044g40000cc2hhdbc77ufp0p13l9g",
|
||||
"video_id": "7135137277650439430",
|
||||
"region": "NL",
|
||||
"title": "That that you do most, will be that that you do best #jawanzakunjufu #NBA #blackthinkers #imhotep #blackhistory #blackpeople #africancommunity #blackpower #blackunity #panafricanism #fyp #foryou #panafrican #afrikancommunity #blackconsciousness #blackcommunity #unlearnandrelearn #blackexcellence #problack #knowthyself #ubuntu",
|
||||
"cover": "https://p16-sign-va.tiktokcdn.com/obj/tos-maliva-p-0068/09d637f654744d62826facb8a685a44f_1661278613?x-expires=1703124000&x-signature=85qRg1ex%2FkscTT8sSfRpq7XSvos%3D&s=SEARCH&se=false&sh=&sc=dynamic_cover&l=20231220021905B0B31567CD73EF000289",
|
||||
"origin_cover": "https://p16-sign-va.tiktokcdn.com/tos-maliva-p-0068/bbc3f2523b50459099bec727131b73c0_1661278613~tplv-tiktokx-360p.webp?x-expires=1703124000&x-signature=PIvHym7TJAU%2BXGScgIuFGaRVkzI%3D&s=SEARCH&se=false&sh=&sc=feed_cover&l=20231220021905B0B31567CD73EF000289",
|
||||
"duration": 60,
|
||||
"play": "https://v19.tiktokcdn-us.com/1f8f8c36c8e7b1d288c09be54090c378/6582a3b6/video/tos/useast2a/tos-useast2a-ve-0068c002/2a36ec9fd62e44a79fd341682312a92e/?a=1233&ch=0&cr=13&dr=0&lr=all&cd=0%7C0%7C0%7C&cv=1&br=1212&bt=606&bti=NEBzNTY6QGo6OjZALnAjNDQuYCMxNDNg&cs=0&ds=6&ft=kJrRfy7oZtc0PD1Agptxg9wp6DhXvEeC~&mime_type=video_mp4&qs=0&rc=Zjs1ZjxlNWY8ZzllNTw5ZkBpam80M2k6ZmdrZjMzNzczM0BjNjBfXzA1NWAxLWAxNDMzYSM2MG1tcjRfZS9gLS1kMTZzcw%3D%3D&l=20231220021905B0B31567CD73EF000289&btag=e00010000",
|
||||
"wmplay": "https://v19.tiktokcdn-us.com/a48c0af8c7cdf127e4c085c61adfbb89/6582a3b6/video/tos/useast2a/tos-useast2a-pve-0068/7d4538584c7e424a9aa60e034d02c6e8/?a=1233&ch=0&cr=13&dr=0&lr=all&cd=0%7C0%7C0%7C&cv=1&br=1254&bt=627&bti=NEBzNTY6QGo6OjZALnAjNDQuYCMxNDNg&cs=0&ds=3&ft=kJrRfy7oZtc0PD1Agptxg9wp6DhXvEeC~&mime_type=video_mp4&qs=0&rc=NjVkaDxlZ2c0aWllZ2Q1NkBpam80M2k6ZmdrZjMzNzczM0BeLl4uNmA0X2AxMGFfLy80YSM2MG1tcjRfZS9gLS1kMTZzcw%3D%3D&l=20231220021905B0B31567CD73EF000289&btag=e00010000",
|
||||
"music": "https://sf16-ies-music-va.tiktokcdn.com/obj/musically-maliva-obj/7135137305857460998.mp3",
|
||||
"music_info": {
|
||||
"id": "7135137291257154310",
|
||||
"title": "original sound - afrikancommunity",
|
||||
"play": "https://sf16-ies-music-va.tiktokcdn.com/obj/musically-maliva-obj/7135137305857460998.mp3",
|
||||
"cover": "https://p16-sign-va.tiktokcdn.com/tos-maliva-avt-0068/93c33be7e299ea8cf1762b3ff08175bc~c5_1080x1080.jpeg?x-expires=1703124000&x-signature=kP1cVyhAwezOqb3NoY3PVxO9Oyo%3D",
|
||||
"author": "afrikancommunity",
|
||||
"original": true,
|
||||
"duration": 60,
|
||||
"album": ""
|
||||
},
|
||||
"play_count": 3509817,
|
||||
"digg_count": 406909,
|
||||
"comment_count": 5287,
|
||||
"share_count": 27964,
|
||||
"download_count": 12202,
|
||||
"create_time": 1661278609,
|
||||
"anchors": null,
|
||||
"anchors_extras": "",
|
||||
"is_ad": false,
|
||||
"commerce_info": {
|
||||
"adv_promotable": false,
|
||||
"auction_ad_invited": false,
|
||||
"branded_content_type": 0,
|
||||
"with_comment_filter_words": false
|
||||
},
|
||||
"commercial_video_info": "",
|
||||
"author": {
|
||||
"id": "6813642741462287365",
|
||||
"unique_id": "afrikancommunity",
|
||||
"nickname": "afrikancommunity",
|
||||
"avatar": "https://p16-sign-va.tiktokcdn.com/tos-maliva-avt-0068/93c33be7e299ea8cf1762b3ff08175bc~c5_300x300.jpeg?x-expires=1703124000&x-signature=QIJZsNfd2YOgDg8lMGEqGQeam24%3D"
|
||||
},
|
||||
"is_top": 0
|
||||
},
|
||||
{
|
||||
"aweme_id": "v09044g40000cbhc463c77u5po59g8fg",
|
||||
"video_id": "7125471227543670022",
|
||||
"region": "NL",
|
||||
"title": "They don\u2019t tell us about ourselves. #blackpeople #africancommunity #blackpower #blackunity #burnaboy #blackhistory #ancientafrica #panafricanism #fyp #foryou #panafrican #afrikancommunity #blackconsciousness #blackcommunity #unlearnandrelearn #blackexcellence #problack #knowthyself #ubuntu",
|
||||
"cover": "https://p16-sign-va.tiktokcdn.com/obj/tos-maliva-p-0068/27058259ac9c4f7996ed15f7d35fcb42_1659028058?x-expires=1703124000&x-signature=zt3tBpttXmjohAqGGMuQZU0Ef%2FU%3D&s=SEARCH&se=false&sh=&sc=dynamic_cover&l=20231220021905B0B31567CD73EF000289",
|
||||
"origin_cover": "https://p16-sign-va.tiktokcdn.com/tos-maliva-p-0068/5b086bdb9aa644448d1219834f894637_1659028057~tplv-tiktokx-360p.webp?x-expires=1703124000&x-signature=0%2BVWKeBo8T%2FHZ2bfJx7bGxsnNUo%3D&s=SEARCH&se=false&sh=&sc=feed_cover&l=20231220021905B0B31567CD73EF000289",
|
||||
"duration": 43,
|
||||
"play": "https://v16m.tiktokcdn-us.com/d5828b1ca12fa0904ce987640b5debfc/6582a3a5/video/tos/useast2a/tos-useast2a-ve-0068c003/1dab5ac30d07444fb1115e537e4656fd/?a=1233&ch=0&cr=13&dr=0&lr=all&cd=0%7C0%7C0%7C&cv=1&br=1362&bt=681&bti=NEBzNTY6QGo6OjZALnAjNDQuYCMxNDNg&cs=0&ds=6&ft=kJrRfy7oZtc0PD1Agptxg9wp6DhXvEeC~&mime_type=video_mp4&qs=0&rc=aDU7aDVpNWdmOGk6NjhoNkBpajs8cjg6ZjlmZTMzNzczM0BeYmEzYi8xXzExYTYvMi1fYSNjZDJtcjQwMWVgLS1kMTZzcw%3D%3D&l=20231220021905B0B31567CD73EF000289&btag=e00008000",
|
||||
"wmplay": "https://v16m.tiktokcdn-us.com/b8f7853d30e428a6af5bc705ffeb227d/6582a3a5/video/tos/useast2a/tos-useast2a-ve-0068c004/a8726699cd774e08b10c124967f1095c/?a=1233&ch=0&cr=13&dr=0&lr=all&cd=0%7C0%7C0%7C&cv=1&br=1398&bt=699&bti=NEBzNTY6QGo6OjZALnAjNDQuYCMxNDNg&cs=0&ds=3&ft=kJrRfy7oZtc0PD1Agptxg9wp6DhXvEeC~&mime_type=video_mp4&qs=0&rc=ZzNlM2k0OWg2OGhpNWVkaUBpajs8cjg6ZjlmZTMzNzczM0AyLmEwL2IvXi4xY2MxMTAuYSNjZDJtcjQwMWVgLS1kMTZzcw%3D%3D&l=20231220021905B0B31567CD73EF000289&btag=e00008000",
|
||||
"music": "https://sf16-ies-music-va.tiktokcdn.com/obj/musically-maliva-obj/7125471236305849094.mp3",
|
||||
"music_info": {
|
||||
"id": "7125471221475527429",
|
||||
"title": "original sound - afrikancommunity",
|
||||
"play": "https://sf16-ies-music-va.tiktokcdn.com/obj/musically-maliva-obj/7125471236305849094.mp3",
|
||||
"cover": "https://p16-sign-va.tiktokcdn.com/tos-maliva-avt-0068/93c33be7e299ea8cf1762b3ff08175bc~c5_1080x1080.jpeg?x-expires=1703124000&x-signature=kP1cVyhAwezOqb3NoY3PVxO9Oyo%3D",
|
||||
"author": "afrikancommunity",
|
||||
"original": true,
|
||||
"duration": 43,
|
||||
"album": ""
|
||||
},
|
||||
"play_count": 2663740,
|
||||
"digg_count": 289424,
|
||||
"comment_count": 4539,
|
||||
"share_count": 16995,
|
||||
"download_count": 62151,
|
||||
"create_time": 1659028056,
|
||||
"anchors": null,
|
||||
"anchors_extras": "",
|
||||
"is_ad": false,
|
||||
"commerce_info": {
|
||||
"adv_promotable": false,
|
||||
"auction_ad_invited": false,
|
||||
"branded_content_type": 0,
|
||||
"with_comment_filter_words": false
|
||||
},
|
||||
"commercial_video_info": "",
|
||||
"author": {
|
||||
"id": "6813642741462287365",
|
||||
"unique_id": "afrikancommunity",
|
||||
"nickname": "afrikancommunity",
|
||||
"avatar": "https://p16-sign-va.tiktokcdn.com/tos-maliva-avt-0068/93c33be7e299ea8cf1762b3ff08175bc~c5_300x300.jpeg?x-expires=1703124000&x-signature=QIJZsNfd2YOgDg8lMGEqGQeam24%3D"
|
||||
},
|
||||
"is_top": 0
|
||||
},
|
||||
{
|
||||
"aweme_id": "v09044g40000cht15sjc77u6asjve4b0",
|
||||
"video_id": "7240121196505812229",
|
||||
"region": "TR",
|
||||
"title": "#linux #windows #windows10 #windows8 #windows81 #windows7 #windowsvista #windowsxp #microsoft #gnome #macos #apple #arch #ubuntu #comedyvideo #komedi #komik #fyp #kesfet #ke\u015ffet #turkey #turkiye #teknoloji #bili\u015fim #bilisim #pcsystem #2023 #intelcore #bilgisayar #nvidiageforce #gamer #edit #pcbuild #phonk #amdryzen5000series #61 #trabzon ",
|
||||
"cover": "https://p16-sign-va.tiktokcdn.com/obj/tos-maliva-p-0068/912a019391e84924a0feba2bafc5833c_1685722084?x-expires=1703124000&x-signature=L2s%2BqZmvLKnWjgFn1LIQkRqb61M%3D&s=SEARCH&se=false&sh=&sc=dynamic_cover&l=20231220021905B0B31567CD73EF000289",
|
||||
"origin_cover": "https://p16-sign-va.tiktokcdn.com/tos-maliva-p-0068/ee011f41126140cea95ed09e2f928eb9_1685722084~tplv-tiktokx-360p.webp?x-expires=1703124000&x-signature=xo8psLpjw39%2BJiowhbMq8JT5vNs%3D&s=SEARCH&se=false&sh=&sc=feed_cover&l=20231220021905B0B31567CD73EF000289",
|
||||
"duration": 14,
|
||||
"play": "https://v16m.tiktokcdn-us.com/6f76e364f63a124e0cecd737216514a4/6582a389/video/tos/useast2a/tos-useast2a-ve-0068c001/o0cgJIJyAXLIhZSIjksCCuf2eDo6Qg80XrGebn/?a=1233&ch=0&cr=13&dr=0&lr=all&cd=0%7C0%7C0%7C&cv=1&br=3484&bt=1742&bti=NEBzNTY6QGo6OjZALnAjNDQuYCMxNDNg&cs=0&ds=6&ft=kJrRfy7oZtc0PD1Agptxg9wp6DhXvEeC~&mime_type=video_mp4&qs=0&rc=ZDQzaTdmO2Q1MzppNjxmZEBpMzd5djk6ZnY0azMzNzczM0AwYF9eLmI2Ni0xMjNjLTJjYSNfYmdecjRnMnFgLS1kMTZzcw%3D%3D&l=20231220021905B0B31567CD73EF000289&btag=e00008000",
|
||||
"wmplay": "https://v16m.tiktokcdn-us.com/8466b29aabc35b2e0d1cae354b5bc489/6582a389/video/tos/useast2a/tos-useast2a-pve-0068/oUr6jIDrepkDIQtuCAUQEMLbyfBgqeInQ3SCWh/?a=1233&ch=0&cr=13&dr=0&lr=all&cd=0%7C0%7C0%7C&cv=1&br=3566&bt=1783&bti=NEBzNTY6QGo6OjZALnAjNDQuYCMxNDNg&cs=0&ds=3&ft=kJrRfy7oZtc0PD1Agptxg9wp6DhXvEeC~&mime_type=video_mp4&qs=0&rc=OztpNzQ1aDNoPDtoOjQ8Z0BpMzd5djk6ZnY0azMzNzczM0AyLTEvLzFiNS0xLjFeMzVjYSNfYmdecjRnMnFgLS1kMTZzcw%3D%3D&l=20231220021905B0B31567CD73EF000289&btag=e00008000",
|
||||
"music": "https://sf16-ies-music-va.tiktokcdn.com/obj/musically-maliva-obj/7240121223304235782.mp3",
|
||||
"music_info": {
|
||||
"id": "7240121203456428805",
|
||||
"title": "original sound - drizzlep6",
|
||||
"play": "https://sf16-ies-music-va.tiktokcdn.com/obj/musically-maliva-obj/7240121223304235782.mp3",
|
||||
"cover": "https://p16-sign-va.tiktokcdn.com/tos-maliva-avt-0068/251581f4d1594f04ea2f325ff4cc2a07~c5_1080x1080.jpeg?x-expires=1703124000&x-signature=2LG3zWl2%2B7QD8BPwl4NTwXj2bXw%3D",
|
||||
"author": "Milli G\u00f6r\u00fc\u015f\u00e7\u00fc Volkan \ud83c\udf3f\ud83c\udf41",
|
||||
"original": true,
|
||||
"duration": 15,
|
||||
"album": ""
|
||||
},
|
||||
"play_count": 1704069,
|
||||
"digg_count": 247851,
|
||||
"comment_count": 29,
|
||||
"share_count": 40070,
|
||||
"download_count": 491,
|
||||
"create_time": 1685722082,
|
||||
"anchors": null,
|
||||
"anchors_extras": "",
|
||||
"is_ad": false,
|
||||
"commerce_info": {
|
||||
"adv_promotable": false,
|
||||
"auction_ad_invited": false,
|
||||
"branded_content_type": 0,
|
||||
"with_comment_filter_words": false
|
||||
},
|
||||
"commercial_video_info": "",
|
||||
"author": {
|
||||
"id": "7214552603395916805",
|
||||
"unique_id": "drizzlep6",
|
||||
"nickname": "Milli G\u00f6r\u00fc\u015f\u00e7\u00fc Volkan \ud83c\udf3f\ud83c\udf41",
|
||||
"avatar": "https://p16-sign-va.tiktokcdn.com/tos-maliva-avt-0068/251581f4d1594f04ea2f325ff4cc2a07~c5_300x300.jpeg?x-expires=1703124000&x-signature=VNzWdrg82BH2GPcFrY2f5AOs7OQ%3D"
|
||||
},
|
||||
"is_top": 0
|
||||
},
|
||||
{
|
||||
"aweme_id": "v09044g40000cghd6ujc77ucrvaksck0",
|
||||
"video_id": "7215562429403565317",
|
||||
"region": "AT",
|
||||
"title": "would be a shame if i forgot to turn of the network #virtualmachine #vm #virtualbox #windows #linux #ubuntu ",
|
||||
"cover": "https://p16-sign-va.tiktokcdn.com/obj/tos-maliva-p-0068/5fe4d7b7b2524275af1b0e245641ff49_1680004049?x-expires=1703124000&x-signature=xjALXpMVrtWAlD4JQx47n3PLCHw%3D&s=SEARCH&se=false&sh=&sc=dynamic_cover&l=20231220021905B0B31567CD73EF000289",
|
||||
"origin_cover": "https://p16-sign-va.tiktokcdn.com/tos-maliva-p-0068/c9d3e9b708164fe0b78d35b47bba445b_1680004049~tplv-tiktokx-360p.webp?x-expires=1703124000&x-signature=aRETt6Wk39aE1F4zOKWceUGQFKY%3D&s=SEARCH&se=false&sh=&sc=feed_cover&l=20231220021905B0B31567CD73EF000289",
|
||||
"duration": 7,
|
||||
"play": "https://v16m.tiktokcdn-us.com/23adf16946df423842b6b0ce4eb05d25/6582a381/video/tos/useast2a/tos-useast2a-pve-0068/okLmMJhUIgoy9xEN90fkidIOo9mVqCA3zAWIDM/?a=1233&ch=0&cr=13&dr=0&lr=all&cd=0%7C0%7C0%7C&cv=1&br=414&bt=207&bti=NEBzNTY6QGo6OjZALnAjNDQuYCMxNDNg&cs=0&ds=6&ft=kJrRfy7oZtc0PD1Agptxg9wp6DhXvEeC~&mime_type=video_mp4&qs=0&rc=NTtnZWk3ZWU5aTlpZTc5PEBpM2ZueWY6ZnhnajMzNzczM0AzXzFiXzEtXzExYWMuMjM2YSNocF5vcjRnM2VgLS1kMTZzcw%3D%3D&l=20231220021905B0B31567CD73EF000289&btag=e00008000",
|
||||
"wmplay": "https://v16m.tiktokcdn-us.com/5006f2b9180bc17f23aa2c733bd751a2/6582a381/video/tos/useast2a/tos-useast2a-ve-0068c002/o4NDMgNh4oE9y9ok3UAmIOAaY9IPfVC7zMsW0i/?a=1233&ch=0&cr=13&dr=0&lr=all&cd=0%7C0%7C0%7C&cv=1&br=442&bt=221&bti=NEBzNTY6QGo6OjZALnAjNDQuYCMxNDNg&cs=0&ds=3&ft=kJrRfy7oZtc0PD1Agptxg9wp6DhXvEeC~&mime_type=video_mp4&qs=0&rc=NWhlNmdoZTRlZDw0NzVmPEBpM2ZueWY6ZnhnajMzNzczM0A1MjAxMWMzNTAxXjAzXmAuYSNocF5vcjRnM2VgLS1kMTZzcw%3D%3D&l=20231220021905B0B31567CD73EF000289&btag=e00008000",
|
||||
"music": "https://sf16-ies-music-sg.tiktokcdn.com/obj/tos-alisg-ve-2774/94e9293d28a3496981f09994bdb78107",
|
||||
"music_info": {
|
||||
"id": "6855250725862115330",
|
||||
"title": "Coconut Mall (From \"Mario Kart Wii\")",
|
||||
"play": "https://sf16-ies-music-sg.tiktokcdn.com/obj/tos-alisg-ve-2774/94e9293d28a3496981f09994bdb78107",
|
||||
"cover": "https://p16-sg.tiktokcdn.com/aweme/720x720/tos-alisg-v-2774/a8f7d19a20714d9f9e8adc4e69d0708c.jpeg",
|
||||
"author": "",
|
||||
"original": false,
|
||||
"duration": 60,
|
||||
"album": "Mario Kart Wii, The Themes"
|
||||
},
|
||||
"play_count": 1526953,
|
||||
"digg_count": 194938,
|
||||
"comment_count": 408,
|
||||
"share_count": 3334,
|
||||
"download_count": 748,
|
||||
"create_time": 1680004048,
|
||||
"anchors": null,
|
||||
"anchors_extras": "",
|
||||
"is_ad": false,
|
||||
"commerce_info": {
|
||||
"adv_promotable": false,
|
||||
"auction_ad_invited": false,
|
||||
"branded_content_type": 0,
|
||||
"with_comment_filter_words": false
|
||||
},
|
||||
"commercial_video_info": "",
|
||||
"author": {
|
||||
"id": "6539155051389129743",
|
||||
"unique_id": "robert_juergerer",
|
||||
"nickname": "Robert J\u00fcrgerer",
|
||||
"avatar": "https://p16-sign-va.tiktokcdn.com/tos-maliva-avt-0068/8a0e92e0a08bbab2c6c25a13a73ddb35~c5_300x300.jpeg?x-expires=1703124000&x-signature=fREAm5jTL1lkhWduDuVUiFZsP9o%3D"
|
||||
},
|
||||
"is_top": 0
|
||||
},
|
||||
{
|
||||
"aweme_id": "v09044g40000cbmogv3c77ueej864sc0",
|
||||
"video_id": "7128504907803397382",
|
||||
"region": "NL",
|
||||
"title": "A new type of thinking #malcolmx #blackleaders #blackpeople #africancommunity #blackpower #blackunity #panafricanism #fyp #foryou #panafrican #afrikancommunity #blackconsciousness #blackcommunity #unlearnandrelearn #blackexcellence #problack #knowthyself #ubuntu",
|
||||
"cover": "https://p16-sign-va.tiktokcdn.com/obj/tos-maliva-p-0068/faca00b819ce4e9bad1ec0cb8cbe6d63_1659734391?x-expires=1703124000&x-signature=iuELTcdjUHK1EjDlrDB%2FYRaEdws%3D&s=SEARCH&se=false&sh=&sc=dynamic_cover&l=20231220021905B0B31567CD73EF000289",
|
||||
"origin_cover": "https://p16-sign-va.tiktokcdn.com/tos-maliva-p-0068/13d5e51f74a94aaab819798d08c1f86d_1659734391~tplv-tiktokx-360p.webp?x-expires=1703124000&x-signature=FMv9WMW2COiSjgkJyO437KGKCBU%3D&s=SEARCH&se=false&sh=&sc=feed_cover&l=20231220021905B0B31567CD73EF000289",
|
||||
"duration": 52,
|
||||
"play": "https://v19.tiktokcdn-us.com/5a0e715186b2fbfc8132b11429969df5/6582a3ae/video/tos/useast2a/tos-useast2a-pve-0068/352bd236bd0b4a3a8d2213fa503d0a9a/?a=1233&ch=0&cr=13&dr=0&lr=all&cd=0%7C0%7C0%7C&cv=1&br=1208&bt=604&bti=NEBzNTY6QGo6OjZALnAjNDQuYCMxNDNg&cs=0&ds=6&ft=kJrRfy7oZtc0PD1Agptxg9wp6DhXvEeC~&mime_type=video_mp4&qs=0&rc=aDNmaTpmZDQ1ZzlkODlnNUBpM3Y5bWg6ZnlyZTMzNzczM0A0MTU0MS4vXmMxMTE1LzQvYSNgMTVicjQwZGpgLS1kMTZzcw%3D%3D&l=20231220021905B0B31567CD73EF000289&btag=e00008000",
|
||||
"wmplay": "https://v19.tiktokcdn-us.com/76add4e2a195de9c9b2cc8a1fef7b93b/6582a3ae/video/tos/useast2a/tos-useast2a-ve-0068c004/81c10525667444f889c96ca540674e07/?a=1233&ch=0&cr=13&dr=0&lr=all&cd=0%7C0%7C0%7C&cv=1&br=1244&bt=622&bti=NEBzNTY6QGo6OjZALnAjNDQuYCMxNDNg&cs=0&ds=3&ft=kJrRfy7oZtc0PD1Agptxg9wp6DhXvEeC~&mime_type=video_mp4&qs=0&rc=Nzw1Zjs5OWRoNjU1NmlnZkBpM3Y5bWg6ZnlyZTMzNzczM0BeLzFiXjJjX2ExMzRhMzNjYSNgMTVicjQwZGpgLS1kMTZzcw%3D%3D&l=20231220021905B0B31567CD73EF000289&btag=e00008000",
|
||||
"music": "https://sf16-ies-music-va.tiktokcdn.com/obj/musically-maliva-obj/7128504954595412741.mp3",
|
||||
"music_info": {
|
||||
"id": "7128504937776679686",
|
||||
"title": "original sound - afrikancommunity",
|
||||
"play": "https://sf16-ies-music-va.tiktokcdn.com/obj/musically-maliva-obj/7128504954595412741.mp3",
|
||||
"cover": "https://p16-sign-va.tiktokcdn.com/tos-maliva-avt-0068/93c33be7e299ea8cf1762b3ff08175bc~c5_1080x1080.jpeg?x-expires=1703124000&x-signature=kP1cVyhAwezOqb3NoY3PVxO9Oyo%3D",
|
||||
"author": "afrikancommunity",
|
||||
"original": true,
|
||||
"duration": 52,
|
||||
"album": ""
|
||||
},
|
||||
"play_count": 1925864,
|
||||
"digg_count": 187364,
|
||||
"comment_count": 4708,
|
||||
"share_count": 22382,
|
||||
"download_count": 24979,
|
||||
"create_time": 1659734390,
|
||||
"anchors": null,
|
||||
"anchors_extras": "",
|
||||
"is_ad": false,
|
||||
"commerce_info": {
|
||||
"adv_promotable": false,
|
||||
"auction_ad_invited": false,
|
||||
"branded_content_type": 0,
|
||||
"with_comment_filter_words": false
|
||||
},
|
||||
"commercial_video_info": "",
|
||||
"author": {
|
||||
"id": "6813642741462287365",
|
||||
"unique_id": "afrikancommunity",
|
||||
"nickname": "afrikancommunity",
|
||||
"avatar": "https://p16-sign-va.tiktokcdn.com/tos-maliva-avt-0068/93c33be7e299ea8cf1762b3ff08175bc~c5_300x300.jpeg?x-expires=1703124000&x-signature=QIJZsNfd2YOgDg8lMGEqGQeam24%3D"
|
||||
},
|
||||
"is_top": 0
|
||||
},
|
||||
{
|
||||
"aweme_id": "v09044g40000ck0isu3c77u97chrtho0",
|
||||
"video_id": "7278150686989159685",
|
||||
"region": "ZA",
|
||||
"title": "#music #amapiano #mellowandsleazy #tmanxpress #SAMA28 ",
|
||||
"cover": "https://p16-sign-va.tiktokcdn.com/obj/tos-maliva-p-0068/4214065b3b6f4009b2e4808493fbd2d0_1694576516?x-expires=1703124000&x-signature=QyQVCscBgVlZQ8bodf4uS6htdfc%3D&s=SEARCH&se=false&sh=&sc=dynamic_cover&l=20231220021905B0B31567CD73EF000289",
|
||||
"origin_cover": "https://p16-sign-va.tiktokcdn.com/tos-maliva-p-0068/d06fe6a869dc4f869f18a9e1ed0b3887_1694576523~tplv-tiktokx-360p.webp?x-expires=1703124000&x-signature=e7hRfKon9cvBKqt8Y8%2BkxvIwvlg%3D&s=SEARCH&se=false&sh=&sc=feed_cover&l=20231220021905B0B31567CD73EF000289",
|
||||
"duration": 59,
|
||||
"play": "https://v16m.tiktokcdn-us.com/4dbf46e2875addad8d023ad4a99249e7/6582a3b5/video/tos/useast2a/tos-useast2a-ve-0068c003/okMkjuDIQBQl4vAbkXeJCkA9gy5dfEAdnBSRgS/?a=1233&ch=0&cr=13&dr=0&lr=all&cd=0%7C0%7C0%7C&cv=1&br=1718&bt=859&bti=NEBzNTY6QGo6OjZALnAjNDQuYCMxNDNg&cs=0&ds=6&ft=kJrRfy7oZtc0PD1Agptxg9wp6DhXvEeC~&mime_type=video_mp4&qs=4&rc=NDxoZmU3Zjc3Njg0aTVlNkBpM2t1Zjw6ZnhsbjMzNzczM0AzYjBiYTEuNmIxNTRhMWAyYSNscWU0cjQwcC1gLS1kMTZzcw%3D%3D&l=20231220021905B0B31567CD73EF000289&btag=e00010000",
|
||||
"wmplay": "https://v16m.tiktokcdn-us.com/d4b759e76706f51d9400f0c172b11afc/6582a3b5/video/tos/useast2a/tos-useast2a-pve-0068/owIAJI4Endn5SBLxQDb5AZQ7eedkLlBEzSwRHB/?a=1233&ch=0&cr=13&dr=0&lr=all&cd=0%7C0%7C0%7C&cv=1&br=3264&bt=1632&bti=NEBzNTY6QGo6OjZALnAjNDQuYCMxNDNg&cs=0&ds=3&ft=kJrRfy7oZtc0PD1Agptxg9wp6DhXvEeC~&mime_type=video_mp4&qs=0&rc=ODw6PDQ3PDhoOTU1OzppZkBpM2t1Zjw6ZnhsbjMzNzczM0AzLTU0YDQ0XmExMWFeXjFeYSNscWU0cjQwcC1gLS1kMTZzcw%3D%3D&l=20231220021905B0B31567CD73EF000289&btag=e00010000",
|
||||
"music": "https://sf16-ies-music-sg.tiktokcdn.com/obj/tos-alisg-ve-2774/o0DzM4MSCXFMRonTCAb3e2FgOYIfy1weDJpJSa",
|
||||
"music_info": {
|
||||
"id": "7264936944639330305",
|
||||
"title": "Imnandi lento (feat. Tman Xpress)",
|
||||
"play": "https://sf16-ies-music-sg.tiktokcdn.com/obj/tos-alisg-ve-2774/o0DzM4MSCXFMRonTCAb3e2FgOYIfy1weDJpJSa",
|
||||
"cover": "https://p16-sg.tiktokcdn.com/aweme/720x720/tos-alisg-v-2774/oEk3DTvnEAJSBQ6tESAczBCoAZetLQbGugfn7D.jpeg",
|
||||
"author": "",
|
||||
"original": false,
|
||||
"duration": 60,
|
||||
"album": "Imnandi lento (feat. Tman Xpress)"
|
||||
},
|
||||
"play_count": 3087192,
|
||||
"digg_count": 182514,
|
||||
"comment_count": 652,
|
||||
"share_count": 11954,
|
||||
"download_count": 38,
|
||||
"create_time": 1694576513,
|
||||
"anchors": null,
|
||||
"anchors_extras": "",
|
||||
"is_ad": true,
|
||||
"commerce_info": {
|
||||
"ad_source": 1,
|
||||
"adv_promotable": true,
|
||||
"auction_ad_invited": false,
|
||||
"bc_label_test_text": "Paid partnership",
|
||||
"branded_content_type": 4,
|
||||
"with_comment_filter_words": false
|
||||
},
|
||||
"commercial_video_info": "",
|
||||
"author": {
|
||||
"id": "7276322982490014725",
|
||||
"unique_id": "ubuntuedits",
|
||||
"nickname": "UBUNTU",
|
||||
"avatar": "https://p16-sign-va.tiktokcdn.com/tos-maliva-avt-0068/e9229e849809078be4bb64b959b4e08d~c5_300x300.jpeg?x-expires=1703124000&x-signature=eRgWDhD7lrp6A7r1IOKMfRggyPY%3D"
|
||||
},
|
||||
"is_top": 0
|
||||
}
|
||||
],
|
||||
"cursor": 10,
|
||||
"hasMore": true
|
||||
}
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
import json
|
||||
import os
|
||||
from googleapiclient.discovery import build
|
||||
import socket
|
||||
socket.setdefaulttimeout(500)
|
||||
|
||||
|
||||
def search_youtube(api_key, query, max_results=50):
|
||||
youtube = build('youtube', 'v3', developerKey=api_key)
|
||||
|
||||
search_response = youtube.search().list(
|
||||
q=query,
|
||||
part="id,snippet",
|
||||
maxResults=max_results,
|
||||
type="video"
|
||||
).execute()
|
||||
|
||||
videos = []
|
||||
|
||||
for search_result in search_response.get("items", []):
|
||||
if search_result["id"]["kind"] == "youtube#video":
|
||||
video_id = search_result["id"]["videoId"]
|
||||
video_metadata = get_video_metadata(api_key, video_id)
|
||||
videos.append(video_metadata)
|
||||
|
||||
return videos
|
||||
|
||||
|
||||
def get_video_metadata(api_key, video_id):
|
||||
youtube = build('youtube', 'v3', developerKey=api_key)
|
||||
|
||||
request = youtube.videos().list(
|
||||
part="snippet,contentDetails,statistics",
|
||||
id=video_id
|
||||
)
|
||||
response = request.execute()
|
||||
return response
|
||||
|
||||
|
||||
api_key = 'AIzaSyDI_BBExs-HypVZFxgnR5tj5S6-uKyU4vk' # Replace with your actual API key
|
||||
|
||||
# Search for videos related to "VLC player"
|
||||
vlc_related_videos = search_youtube(api_key, "LibreOffice Impress Tutorial", max_results=10)
|
||||
|
||||
# create data folder if not exist
|
||||
if not os.path.exists("data"):
|
||||
os.makedirs("data")
|
||||
|
||||
for video in vlc_related_videos:
|
||||
# store the video metadata into a json file
|
||||
with open(f"data/{video['etag']}.json", "w") as f:
|
||||
json.dump(video, f, indent=4)
|
||||
@@ -1,52 +0,0 @@
|
||||
import json
|
||||
import os
|
||||
from googleapiclient.discovery import build
|
||||
import socket
|
||||
socket.setdefaulttimeout(500)
|
||||
|
||||
|
||||
def search_youtube(api_key, query, max_results=50):
|
||||
youtube = build('youtube', 'v3', developerKey=api_key)
|
||||
|
||||
search_response = youtube.search().list(
|
||||
q=query,
|
||||
part="id,snippet",
|
||||
maxResults=max_results,
|
||||
type="video"
|
||||
).execute()
|
||||
|
||||
videos = []
|
||||
|
||||
for search_result in search_response.get("items", []):
|
||||
if search_result["id"]["kind"] == "youtube#video":
|
||||
video_id = search_result["id"]["videoId"]
|
||||
video_metadata = get_video_metadata(api_key, video_id)
|
||||
videos.append(video_metadata)
|
||||
|
||||
return videos
|
||||
|
||||
|
||||
def get_video_metadata(api_key, video_id):
|
||||
youtube = build('youtube', 'v3', developerKey=api_key)
|
||||
|
||||
request = youtube.videos().list(
|
||||
part="snippet,contentDetails,statistics",
|
||||
id=video_id
|
||||
)
|
||||
response = request.execute()
|
||||
return response
|
||||
|
||||
|
||||
api_key = 'AIzaSyDI_BBExs-HypVZFxgnR5tj5S6-uKyU4vk' # Replace with your actual API key
|
||||
|
||||
# Search for videos related to "VLC player"
|
||||
vlc_related_videos = search_youtube(api_key, "LibreOffice Calc Tutorial", max_results=10)
|
||||
|
||||
# create data folder if not exist
|
||||
if not os.path.exists("data"):
|
||||
os.makedirs("data")
|
||||
|
||||
for video in vlc_related_videos:
|
||||
# store the video metadata into a json file
|
||||
with open(f"data/{video['etag']}.json", "w") as f:
|
||||
json.dump(video, f, indent=4)
|
||||
@@ -1,52 +0,0 @@
|
||||
import json
|
||||
import os
|
||||
from googleapiclient.discovery import build
|
||||
import socket
|
||||
socket.setdefaulttimeout(500)
|
||||
|
||||
|
||||
def search_youtube(api_key, query, max_results=50):
|
||||
youtube = build('youtube', 'v3', developerKey=api_key)
|
||||
|
||||
search_response = youtube.search().list(
|
||||
q=query,
|
||||
part="id,snippet",
|
||||
maxResults=max_results,
|
||||
type="video"
|
||||
).execute()
|
||||
|
||||
videos = []
|
||||
|
||||
for search_result in search_response.get("items", []):
|
||||
if search_result["id"]["kind"] == "youtube#video":
|
||||
video_id = search_result["id"]["videoId"]
|
||||
video_metadata = get_video_metadata(api_key, video_id)
|
||||
videos.append(video_metadata)
|
||||
|
||||
return videos
|
||||
|
||||
|
||||
def get_video_metadata(api_key, video_id):
|
||||
youtube = build('youtube', 'v3', developerKey=api_key)
|
||||
|
||||
request = youtube.videos().list(
|
||||
part="snippet,contentDetails,statistics",
|
||||
id=video_id
|
||||
)
|
||||
response = request.execute()
|
||||
return response
|
||||
|
||||
|
||||
api_key = 'AIzaSyDI_BBExs-HypVZFxgnR5tj5S6-uKyU4vk' # Replace with your actual API key
|
||||
|
||||
# Search for videos related to "VLC player"
|
||||
vlc_related_videos = search_youtube(api_key, "Thunderbird Tutorial", max_results=10)
|
||||
|
||||
# create data folder if not exist
|
||||
if not os.path.exists("data"):
|
||||
os.makedirs("data")
|
||||
|
||||
for video in vlc_related_videos:
|
||||
# store the video metadata into a json file
|
||||
with open(f"data/{video['etag']}.json", "w") as f:
|
||||
json.dump(video, f, indent=4)
|
||||
@@ -1,52 +0,0 @@
|
||||
import json
|
||||
import os
|
||||
from googleapiclient.discovery import build
|
||||
import socket
|
||||
socket.setdefaulttimeout(500)
|
||||
|
||||
|
||||
def search_youtube(api_key, query, max_results=50):
|
||||
youtube = build('youtube', 'v3', developerKey=api_key)
|
||||
|
||||
search_response = youtube.search().list(
|
||||
q=query,
|
||||
part="id,snippet",
|
||||
maxResults=max_results,
|
||||
type="video"
|
||||
).execute()
|
||||
|
||||
videos = []
|
||||
|
||||
for search_result in search_response.get("items", []):
|
||||
if search_result["id"]["kind"] == "youtube#video":
|
||||
video_id = search_result["id"]["videoId"]
|
||||
video_metadata = get_video_metadata(api_key, video_id)
|
||||
videos.append(video_metadata)
|
||||
|
||||
return videos
|
||||
|
||||
|
||||
def get_video_metadata(api_key, video_id):
|
||||
youtube = build('youtube', 'v3', developerKey=api_key)
|
||||
|
||||
request = youtube.videos().list(
|
||||
part="snippet,contentDetails,statistics",
|
||||
id=video_id
|
||||
)
|
||||
response = request.execute()
|
||||
return response
|
||||
|
||||
|
||||
api_key = 'AIzaSyDI_BBExs-HypVZFxgnR5tj5S6-uKyU4vk' # Replace with your actual API key
|
||||
|
||||
# Search for videos related to "VLC player"
|
||||
vlc_related_videos = search_youtube(api_key, "Ubuntu Desktop Tutorial", max_results=10)
|
||||
|
||||
# create data folder if not exist
|
||||
if not os.path.exists("data"):
|
||||
os.makedirs("data")
|
||||
|
||||
for video in vlc_related_videos:
|
||||
# store the video metadata into a json file
|
||||
with open(f"data/{video['etag']}.json", "w") as f:
|
||||
json.dump(video, f, indent=4)
|
||||
@@ -1,65 +0,0 @@
|
||||
import json
|
||||
import os
|
||||
from googleapiclient.discovery import build
|
||||
|
||||
|
||||
def search_youtube(api_key, query, max_results=50, language="en"):
|
||||
youtube = build('youtube', 'v3', developerKey=api_key)
|
||||
|
||||
videos = []
|
||||
next_page_token = None
|
||||
total_results = 0
|
||||
|
||||
while True:
|
||||
search_response = youtube.search().list(
|
||||
q=query,
|
||||
part="id,snippet",
|
||||
maxResults=max_results,
|
||||
pageToken=next_page_token,
|
||||
type="video",
|
||||
relevanceLanguage=language
|
||||
).execute()
|
||||
|
||||
video_ids = [item['id']['videoId'] for item in search_response.get("items", []) if
|
||||
item['id']['kind'] == 'youtube#video']
|
||||
|
||||
# Fetch metadata for each video
|
||||
videos.extend([get_video_metadata(api_key, video_id) for video_id in video_ids])
|
||||
|
||||
total_results += len(video_ids)
|
||||
next_page_token = search_response.get('nextPageToken')
|
||||
|
||||
if not next_page_token or total_results >= max_results:
|
||||
break
|
||||
|
||||
# Sort videos by view count
|
||||
sorted_videos = sorted(videos, key=lambda x: int(x['items'][0]['statistics']['viewCount']), reverse=True)
|
||||
|
||||
return sorted_videos
|
||||
|
||||
|
||||
def get_video_metadata(api_key, video_id):
|
||||
youtube = build('youtube', 'v3', developerKey=api_key)
|
||||
|
||||
request = youtube.videos().list(
|
||||
part="snippet,contentDetails,statistics",
|
||||
id=video_id
|
||||
)
|
||||
response = request.execute()
|
||||
|
||||
return response
|
||||
|
||||
|
||||
api_key = 'API_KEY' # Replace with your actual API key
|
||||
|
||||
# Search for videos related to "VLC player"
|
||||
vlc_related_videos = search_youtube(api_key, "VLC player", max_results=10)
|
||||
|
||||
# create data folder if not exist
|
||||
if not os.path.exists("data"):
|
||||
os.makedirs("data")
|
||||
|
||||
for video in vlc_related_videos:
|
||||
# store the video metadata into a json file
|
||||
with open(f"data/{video['etag']}.json", "w") as f:
|
||||
json.dump(video, f, indent=4)
|
||||
2
run.py
@@ -216,6 +216,8 @@ def get_unfinished(action_space, use_model, observation_type, result_dir, total_
|
||||
domain_path = os.path.join(target_dir, domain)
|
||||
if os.path.isdir(domain_path):
|
||||
for example_id in os.listdir(domain_path):
|
||||
if example_id == "onboard":
|
||||
continue
|
||||
example_path = os.path.join(domain_path, example_id)
|
||||
if os.path.isdir(example_path):
|
||||
if "result.txt" not in os.listdir(example_path):
|
||||
|
||||
BIN
screenshot.png
|
Before Width: | Height: | Size: 826 KiB |
@@ -1,3 +1,3 @@
|
||||
{
|
||||
"time_limit": "1800"
|
||||
"time_limit": "3600"
|
||||
}
|
||||
@@ -1,350 +0,0 @@
|
||||
import sys, pathlib;
|
||||
|
||||
sys.path.append(str(pathlib.Path(__file__).parents[1]))
|
||||
|
||||
import os
|
||||
import math
|
||||
import json
|
||||
import numpy as np
|
||||
from typing import List
|
||||
from copy import deepcopy
|
||||
|
||||
pynput2pyautogui_key = {
|
||||
"alt_l": "altleft",
|
||||
"alt_r": "altright",
|
||||
}
|
||||
COMMAND_KEYS = ['accept', 'add', 'alt', 'altleft', 'altright', 'apps', 'backspace', 'browserback', 'browserfavorites', 'browserforward', 'browserhome', 'browserrefresh', 'browsersearch', 'browserstop', 'capslock', 'clear', 'convert', 'ctrl', 'ctrlleft', 'ctrlright', 'decimal', 'del', 'delete', 'divide', 'down', 'end', 'enter', 'esc', 'escape', 'execute', 'f1', 'f10', 'f11', 'f12', 'f13', 'f14', 'f15', 'f16', 'f17', 'f18', 'f19', 'f2', 'f20', 'f21', 'f22', 'f23', 'f24', 'f3', 'f4', 'f5', 'f6', 'f7', 'f8', 'f9', 'final', 'fn', 'hanguel', 'hangul', 'hanja', 'help', 'home', 'insert', 'junja', 'kana', 'kanji', 'launchapp1', 'launchapp2', 'launchmail', 'launchmediaselect', 'left', 'modechange', 'multiply', 'nexttrack', 'nonconvert', 'num0', 'num1', 'num2', 'num3', 'num4', 'num5', 'num6', 'num7', 'num8', 'num9', 'numlock', 'pagedown', 'pageup', 'pause', 'pgdn', 'pgup', 'playpause', 'prevtrack', 'print', 'printscreen', 'prntscrn', 'prtsc', 'prtscr', 'return', 'right', 'scrolllock', 'select', 'separator', 'shift', 'shiftleft', 'shiftright', 'sleep', 'stop', 'subtract', 'tab', 'up', 'volumedown', 'volumemute', 'volumeup', 'win', 'winleft', 'winright', 'yen', 'command', 'option', 'optionleft', 'optionright', 'alt_l', 'alt_r']
|
||||
typingkey2str = {
|
||||
"space" : " ",
|
||||
}
|
||||
|
||||
class DuckTrackEventActionConverter:
|
||||
def __init__(self, ):
|
||||
""""""
|
||||
|
||||
### Enumerations ###
|
||||
def move_event_to_action(self, event: dict, action_space: str = "computer_13"):
|
||||
"""Converts a mouse move event to its corresponding action."""
|
||||
if action_space == "computer_13":
|
||||
return {
|
||||
"action_type": "MOVE_TO",
|
||||
"parameters": {
|
||||
"x": event["x"],
|
||||
"y": event["y"]
|
||||
}
|
||||
}
|
||||
elif action_space == "pyautogui":
|
||||
return "pyautogui.moveTo({}, {})".format(event["x"], event["y"])
|
||||
|
||||
def click_event_to_action(self, event: dict, action_space: str = "computer_13"):
|
||||
"""Converts a mouse click event to its corresponding action."""
|
||||
action = {
|
||||
"action_type": None,
|
||||
"parameters": {
|
||||
"button": None
|
||||
}
|
||||
}
|
||||
|
||||
mouse_button = event["button"]
|
||||
mouse_pressed = event["pressed"]
|
||||
|
||||
if mouse_pressed:
|
||||
action["action_type"] = "MOUSE_DOWN"
|
||||
elif not mouse_pressed:
|
||||
action["action_type"] = "MOUSE_UP"
|
||||
else:
|
||||
raise NotImplementedError(mouse_pressed)
|
||||
|
||||
if mouse_button in ["left", "right", "middle"]:
|
||||
action["parameters"]["button"] = mouse_button
|
||||
else:
|
||||
raise NotImplementedError(mouse_button)
|
||||
|
||||
return action
|
||||
|
||||
def press_event_to_action(self, event: dict, action_space: str = "computer_13"):
|
||||
"""Converts a key down event to its corresponding action."""
|
||||
# NOTE: the `key down`, `press` have the same meaning here, while different in pyautogui
|
||||
return {
|
||||
"action_type": "KEY_DOWN",
|
||||
"parameters": {
|
||||
"key": event["name"] if event["name"] not in pynput2pyautogui_key else pynput2pyautogui_key[
|
||||
event["name"]]
|
||||
}
|
||||
}
|
||||
|
||||
def release_event_to_action(self, event: dict, action_space: str = "computer_13"):
|
||||
"""Converts a key release event to its corresponding action."""
|
||||
return {
|
||||
"action_type": "KEY_UP",
|
||||
"parameters": {
|
||||
"key": event["name"] if event["name"] not in pynput2pyautogui_key else pynput2pyautogui_key[
|
||||
event["name"]]
|
||||
}
|
||||
}
|
||||
|
||||
def scroll_event_to_action(self, event: dict, action_space: str = "computer_13"):
|
||||
"""Converts a scroll event to its corresponding action."""
|
||||
return {
|
||||
"action_type": "SCROLL",
|
||||
"parameters": {
|
||||
"dx": event["dx"],
|
||||
"dy": event["dy"]
|
||||
}
|
||||
}
|
||||
|
||||
def event_to_action(self, event: dict, action_space: str = "computer_13"):
|
||||
"""Converts an event to its corresponding action based on the event type."""
|
||||
if event["action"] == "move":
|
||||
return self.move_event_to_action(event)
|
||||
elif event["action"] == "click":
|
||||
return self.click_event_to_action(event)
|
||||
elif event["action"] == "press":
|
||||
return self.press_event_to_action(event)
|
||||
elif event["action"] == "release":
|
||||
return self.release_event_to_action(event)
|
||||
elif event["action"] == "scroll":
|
||||
return self.scroll_event_to_action(event)
|
||||
else:
|
||||
raise NotImplementedError(event["action"])
|
||||
|
||||
### Compressing ###
|
||||
def compress_mouse_move(self, data: List[dict], index: int):
|
||||
"""Compresses consecutive mouse move events into the last move events."""
|
||||
last_move = data[index]
|
||||
while index < len(data) and data[index]["action"] == "move":
|
||||
last_move = data[index]
|
||||
index += 1
|
||||
return last_move, index
|
||||
|
||||
def compress_scroll(self, data: List[dict], index: int):
|
||||
"""Compresses consecutive scroll events into a single scroll event."""
|
||||
last_scroll = data[index]
|
||||
consecutive_dx, consecutive_dy = data[index]["dx"], data[index]["dy"]
|
||||
while index < len(data) and data[index]["action"] == "scroll" and np.sign(data[index]["dx"]) == np.sign(consecutive_dx) and np.sign(data[index]["dy"]) == np.sign(consecutive_dy):
|
||||
last_scroll = data[index]
|
||||
consecutive_dx += data[index]["dx"]
|
||||
consecutive_dy += data[index]["dy"]
|
||||
index += 1
|
||||
last_scroll["dx"], last_scroll["dy"] = consecutive_dx, consecutive_dy
|
||||
return last_scroll, index
|
||||
|
||||
### Converting ###
|
||||
def ducktrack_event_file_to_action(self, ducktrack_event_file: str, out_file: str, compress_move: bool = True, compress_scroll: bool = True, compress_click: bool = True,compress_drag: bool = True, compress_press_key: bool = True, compress_typing: bool = True):
|
||||
"""Converts DuckTrack event data to a list of actions and saves them to a file."""
|
||||
if not os.path.exists(ducktrack_event_file):
|
||||
raise FileNotFoundError(ducktrack_event_file)
|
||||
|
||||
with open(ducktrack_event_file, 'r') as file:
|
||||
events = [json.loads(line) for line in file]
|
||||
|
||||
# Save the compressed actions in a list
|
||||
result = []
|
||||
index = 0
|
||||
presses_to_skip = 0
|
||||
releases_to_skip = 0
|
||||
move_to_skip = 0
|
||||
keys_pressed = []
|
||||
|
||||
# Compress the mouse move events
|
||||
while index < len(events):
|
||||
|
||||
event = events[index]
|
||||
|
||||
def do_mouse_press(button: str, _index: int):
|
||||
|
||||
num_clicks = 0
|
||||
mouse_pressed = True
|
||||
skip_move = 0
|
||||
click_x, click_y = event["x"], event["y"]
|
||||
|
||||
for j, next_event in enumerate(events[index + 1:]):
|
||||
# make sure the time between mouse clicks is less than 500ms
|
||||
if next_event["time_stamp"] - event["time_stamp"] > 0.5:
|
||||
if num_clicks > 0:
|
||||
if result[-1:][0]["action_type"] == "MOVE_TO":
|
||||
result.pop()
|
||||
result.append({
|
||||
"action_type": "CLICK",
|
||||
"parameters": {
|
||||
"button": button,
|
||||
"x" : click_x,
|
||||
"y" : click_y,
|
||||
"num_clicks": num_clicks
|
||||
}
|
||||
})
|
||||
return num_clicks-1, num_clicks, _index, skip_move
|
||||
break
|
||||
|
||||
if "x" in next_event and "y" in next_event:
|
||||
# if the mouse moves out of the click radius/rectangle, it is not a click sequence
|
||||
if math.sqrt((next_event["y"] - event["y"]) ** 2 +
|
||||
(next_event["x"] - event["x"]) ** 2) > 4:
|
||||
if num_clicks > 0:
|
||||
if result[-1:][0]["action_type"] == "MOVE_TO":
|
||||
result.pop()
|
||||
result.append({
|
||||
"action_type": "CLICK",
|
||||
"parameters": {
|
||||
"button": button,
|
||||
"x" : click_x,
|
||||
"y" : click_y,
|
||||
"num_clicks": num_clicks
|
||||
}
|
||||
})
|
||||
return num_clicks-1, num_clicks, _index, skip_move
|
||||
break
|
||||
|
||||
if next_event["action"] == "click" and compress_click:
|
||||
if not next_event["pressed"]:
|
||||
num_clicks += 1
|
||||
mouse_pressed = False
|
||||
if num_clicks == 3:
|
||||
if result[-1:][0]["action_type"] == "MOVE_TO":
|
||||
result.pop()
|
||||
result.append({
|
||||
"action_type": "CLICK",
|
||||
"parameters": {
|
||||
"button": button,
|
||||
"x" : click_x,
|
||||
"y" : click_y,
|
||||
"num_clicks": 3
|
||||
}
|
||||
})
|
||||
return 2, 3, _index, skip_move
|
||||
elif next_event["pressed"]:
|
||||
mouse_pressed = True
|
||||
else:
|
||||
raise NotImplementedError(next_event["pressed"])
|
||||
elif next_event["action"] != "click" and not mouse_pressed:
|
||||
if next_event["action"] == "move":
|
||||
if next_event["x"] == click_x and next_event["y"] == click_y:
|
||||
skip_move += 1
|
||||
continue
|
||||
if result[-1:][0]["action_type"] == "MOVE_TO":
|
||||
result.pop()
|
||||
result.append({
|
||||
"action_type": "CLICK",
|
||||
"parameters": {
|
||||
"button": button,
|
||||
"x" : click_x,
|
||||
"y" : click_y,
|
||||
"num_clicks": num_clicks
|
||||
}
|
||||
})
|
||||
return num_clicks-1, num_clicks, _index, skip_move
|
||||
|
||||
# Compress {MOUSE_DOWN, MOVE, MOUSE_UP} into DRAG_TO event
|
||||
elif next_event["action"] == "move" and compress_drag:
|
||||
if next_event["x"] == click_x and next_event["y"] == click_y:
|
||||
skip_move += 1
|
||||
continue
|
||||
last_move, _index = self.compress_mouse_move(events, _index+1)
|
||||
result.append({
|
||||
"action_type": "DRAG_TO",
|
||||
"parameters": {
|
||||
"x": last_move["x"],
|
||||
"y": last_move["y"]
|
||||
}
|
||||
})
|
||||
return 0, 1, _index, skip_move
|
||||
|
||||
result.append({
|
||||
"action_type": "MOUSE_DOWN",
|
||||
"parameters": {
|
||||
"button": button
|
||||
}
|
||||
})
|
||||
return 0, 0, _index, skip_move
|
||||
|
||||
if event["action"] == "move":
|
||||
if move_to_skip > 0:
|
||||
move_to_skip -= 1
|
||||
index += 1
|
||||
continue
|
||||
if compress_move:
|
||||
last_move, index = self.compress_mouse_move(events, index)
|
||||
result.extend([self.event_to_action(last_move)])
|
||||
|
||||
elif event["action"] == "scroll" and compress_scroll:
|
||||
last_scroll, index = self.compress_scroll(events, index)
|
||||
result.extend([self.event_to_action(last_scroll)])
|
||||
|
||||
elif event["action"] == "click":
|
||||
button = event["button"]
|
||||
|
||||
if event["pressed"]:
|
||||
if presses_to_skip == 0:
|
||||
presses, releases, index, moves = do_mouse_press(button, index)
|
||||
presses_to_skip += presses
|
||||
releases_to_skip += releases
|
||||
move_to_skip += moves
|
||||
else:
|
||||
presses_to_skip -= 1
|
||||
else:
|
||||
if releases_to_skip == 0:
|
||||
result.append({
|
||||
"action_type": "MOUSE_UP",
|
||||
"parameters": {
|
||||
"button": button
|
||||
}
|
||||
})
|
||||
else:
|
||||
releases_to_skip -= 1
|
||||
index += 1
|
||||
elif event["action"] == "press" and event["name"] not in COMMAND_KEYS and compress_typing:
|
||||
typing_words = ""
|
||||
while index < len(events) and events[index]["action"] in ["press", "release"] and events[index]["name"] not in COMMAND_KEYS:
|
||||
if events[index]["action"] == "press":
|
||||
keys_pressed.append(events[index]["name"])
|
||||
typing_words += events[index]["name"] if events[index]["name"] not in typingkey2str else typingkey2str[events[index]["name"]]
|
||||
elif events[index]["action"] == "release":
|
||||
keys_pressed.remove(events[index]["name"])
|
||||
index += 1
|
||||
if len(typing_words) > 1:
|
||||
result.append({
|
||||
"action_type": "TYPING",
|
||||
"parameters": {
|
||||
"text": typing_words
|
||||
}
|
||||
})
|
||||
else:
|
||||
result.append({
|
||||
"action_type": "PRESS",
|
||||
"parameters": {
|
||||
"key": typing_words
|
||||
}
|
||||
})
|
||||
elif event["action"] == "press" and compress_press_key:
|
||||
keys_pressed.append(event["name"])
|
||||
result.append({
|
||||
"action_type": "PRESS",
|
||||
"parameters": {
|
||||
"key": event["name"] if event["name"] not in pynput2pyautogui_key else pynput2pyautogui_key[
|
||||
event["name"]]
|
||||
}
|
||||
})
|
||||
index += 1
|
||||
elif event["action"] == "release" and compress_press_key:
|
||||
keys_pressed.remove(event["name"])
|
||||
index += 1
|
||||
else:
|
||||
result.append(self.event_to_action(event))
|
||||
index += 1
|
||||
|
||||
with open(out_file, "w") as f:
|
||||
json.dump(result, f)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
converter = DuckTrackEventActionConverter()
|
||||
converter.ducktrack_event_file_to_action(
|
||||
ducktrack_event_file="complex_clicking.jsonl",
|
||||
out_file="complex_clicking5.json",
|
||||
compress_move=True,
|
||||
compress_scroll=True,
|
||||
compress_click=True,
|
||||
compress_drag=True,
|
||||
compress_press_key=True,
|
||||
compress_typing=True,
|
||||
)
|
||||
@@ -1,111 +0,0 @@
|
||||
[
|
||||
{
|
||||
"action_type": "MOVE_TO",
|
||||
"parameters": {
|
||||
"x": 152,
|
||||
"y": 259
|
||||
}
|
||||
},
|
||||
{
|
||||
"action_type": "MOUSE_DOWN",
|
||||
"parameters": {
|
||||
"button": "left"
|
||||
}
|
||||
},
|
||||
{
|
||||
"action_type": "MOVE_TO",
|
||||
"parameters": {
|
||||
"x": 464,
|
||||
"y": 317
|
||||
}
|
||||
},
|
||||
{
|
||||
"action_type": "MOUSE_UP",
|
||||
"parameters": {
|
||||
"button": "left"
|
||||
}
|
||||
},
|
||||
{
|
||||
"action_type": "MOVE_TO",
|
||||
"parameters": {
|
||||
"x": 466,
|
||||
"y": 317
|
||||
}
|
||||
},
|
||||
{
|
||||
"action_type": "KEY_DOWN",
|
||||
"parameters": {
|
||||
"key": "altleft"
|
||||
}
|
||||
},
|
||||
{
|
||||
"action_type": "KEY_DOWN",
|
||||
"parameters": {
|
||||
"key": "="
|
||||
}
|
||||
},
|
||||
{
|
||||
"action_type": "KEY_UP",
|
||||
"parameters": {
|
||||
"key": "="
|
||||
}
|
||||
},
|
||||
{
|
||||
"action_type": "KEY_UP",
|
||||
"parameters": {
|
||||
"key": "altleft"
|
||||
}
|
||||
},
|
||||
{
|
||||
"action_type": "MOVE_TO",
|
||||
"parameters": {
|
||||
"x": 709,
|
||||
"y": 1047
|
||||
}
|
||||
},
|
||||
{
|
||||
"action_type": "MOUSE_DOWN",
|
||||
"parameters": {
|
||||
"button": "left"
|
||||
}
|
||||
},
|
||||
{
|
||||
"action_type": "MOVE_TO",
|
||||
"parameters": {
|
||||
"x": 709,
|
||||
"y": 1047
|
||||
}
|
||||
},
|
||||
{
|
||||
"action_type": "MOUSE_UP",
|
||||
"parameters": {
|
||||
"button": "left"
|
||||
}
|
||||
},
|
||||
{
|
||||
"action_type": "MOVE_TO",
|
||||
"parameters": {
|
||||
"x": 717,
|
||||
"y": 304
|
||||
}
|
||||
},
|
||||
{
|
||||
"action_type": "MOUSE_DOWN",
|
||||
"parameters": {
|
||||
"button": "left"
|
||||
}
|
||||
},
|
||||
{
|
||||
"action_type": "MOVE_TO",
|
||||
"parameters": {
|
||||
"x": 717,
|
||||
"y": 304
|
||||
}
|
||||
},
|
||||
{
|
||||
"action_type": "MOUSE_UP",
|
||||
"parameters": {
|
||||
"button": "left"
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -1,423 +0,0 @@
|
||||
{"time_stamp": 21028.2899763, "action": "move", "x": 686, "y": 306}
|
||||
{"time_stamp": 21028.2965794, "action": "move", "x": 684, "y": 306}
|
||||
{"time_stamp": 21028.3046644, "action": "move", "x": 678, "y": 306}
|
||||
{"time_stamp": 21028.3126807, "action": "move", "x": 670, "y": 306}
|
||||
{"time_stamp": 21028.3208329, "action": "move", "x": 661, "y": 306}
|
||||
{"time_stamp": 21028.3288313, "action": "move", "x": 645, "y": 306}
|
||||
{"time_stamp": 21028.336626, "action": "move", "x": 625, "y": 306}
|
||||
{"time_stamp": 21028.3445457, "action": "move", "x": 603, "y": 305}
|
||||
{"time_stamp": 21028.3527487, "action": "move", "x": 574, "y": 303}
|
||||
{"time_stamp": 21028.3606394, "action": "move", "x": 544, "y": 301}
|
||||
{"time_stamp": 21028.3688565, "action": "move", "x": 508, "y": 300}
|
||||
{"time_stamp": 21028.3768381, "action": "move", "x": 471, "y": 298}
|
||||
{"time_stamp": 21028.3848709, "action": "move", "x": 430, "y": 296}
|
||||
{"time_stamp": 21028.3926563, "action": "move", "x": 389, "y": 296}
|
||||
{"time_stamp": 21028.4009164, "action": "move", "x": 348, "y": 296}
|
||||
{"time_stamp": 21028.4089388, "action": "move", "x": 313, "y": 296}
|
||||
{"time_stamp": 21028.4171707, "action": "move", "x": 280, "y": 296}
|
||||
{"time_stamp": 21028.4245847, "action": "move", "x": 252, "y": 294}
|
||||
{"time_stamp": 21028.4328148, "action": "move", "x": 225, "y": 294}
|
||||
{"time_stamp": 21028.4406678, "action": "move", "x": 208, "y": 294}
|
||||
{"time_stamp": 21028.4486998, "action": "move", "x": 192, "y": 294}
|
||||
{"time_stamp": 21028.4568529, "action": "move", "x": 177, "y": 294}
|
||||
{"time_stamp": 21028.4647334, "action": "move", "x": 163, "y": 293}
|
||||
{"time_stamp": 21028.4729702, "action": "move", "x": 153, "y": 293}
|
||||
{"time_stamp": 21028.4808044, "action": "move", "x": 143, "y": 293}
|
||||
{"time_stamp": 21028.4889062, "action": "move", "x": 135, "y": 293}
|
||||
{"time_stamp": 21028.4967676, "action": "move", "x": 130, "y": 293}
|
||||
{"time_stamp": 21028.5050544, "action": "move", "x": 124, "y": 293}
|
||||
{"time_stamp": 21028.5127317, "action": "move", "x": 120, "y": 293}
|
||||
{"time_stamp": 21028.520827, "action": "move", "x": 117, "y": 293}
|
||||
{"time_stamp": 21028.5289378, "action": "move", "x": 114, "y": 293}
|
||||
{"time_stamp": 21028.5371078, "action": "move", "x": 111, "y": 293}
|
||||
{"time_stamp": 21028.545514, "action": "move", "x": 107, "y": 293}
|
||||
{"time_stamp": 21028.5527022, "action": "move", "x": 104, "y": 292}
|
||||
{"time_stamp": 21028.5605384, "action": "move", "x": 100, "y": 292}
|
||||
{"time_stamp": 21028.5686583, "action": "move", "x": 96, "y": 291}
|
||||
{"time_stamp": 21028.5766951, "action": "move", "x": 90, "y": 291}
|
||||
{"time_stamp": 21028.5847502, "action": "move", "x": 85, "y": 291}
|
||||
{"time_stamp": 21028.5926223, "action": "move", "x": 79, "y": 290}
|
||||
{"time_stamp": 21028.6007454, "action": "move", "x": 74, "y": 290}
|
||||
{"time_stamp": 21028.6088707, "action": "move", "x": 70, "y": 289}
|
||||
{"time_stamp": 21028.6166501, "action": "move", "x": 67, "y": 289}
|
||||
{"time_stamp": 21028.6249259, "action": "move", "x": 66, "y": 289}
|
||||
{"time_stamp": 21028.6647889, "action": "move", "x": 66, "y": 289}
|
||||
{"time_stamp": 21028.6728642, "action": "move", "x": 68, "y": 288}
|
||||
{"time_stamp": 21028.6807781, "action": "move", "x": 70, "y": 286}
|
||||
{"time_stamp": 21028.6888295, "action": "move", "x": 74, "y": 285}
|
||||
{"time_stamp": 21028.6971027, "action": "move", "x": 77, "y": 284}
|
||||
{"time_stamp": 21028.7046499, "action": "move", "x": 81, "y": 282}
|
||||
{"time_stamp": 21028.7129405, "action": "move", "x": 86, "y": 281}
|
||||
{"time_stamp": 21028.7205325, "action": "move", "x": 91, "y": 279}
|
||||
{"time_stamp": 21028.7285422, "action": "move", "x": 98, "y": 278}
|
||||
{"time_stamp": 21028.7366509, "action": "move", "x": 104, "y": 275}
|
||||
{"time_stamp": 21028.7448279, "action": "move", "x": 110, "y": 275}
|
||||
{"time_stamp": 21028.7527897, "action": "move", "x": 116, "y": 273}
|
||||
{"time_stamp": 21028.7609718, "action": "move", "x": 120, "y": 272}
|
||||
{"time_stamp": 21028.7688693, "action": "move", "x": 124, "y": 271}
|
||||
{"time_stamp": 21028.7766846, "action": "move", "x": 128, "y": 270}
|
||||
{"time_stamp": 21028.7848371, "action": "move", "x": 131, "y": 270}
|
||||
{"time_stamp": 21028.7927773, "action": "move", "x": 133, "y": 268}
|
||||
{"time_stamp": 21028.8007498, "action": "move", "x": 134, "y": 268}
|
||||
{"time_stamp": 21028.8088143, "action": "move", "x": 136, "y": 268}
|
||||
{"time_stamp": 21028.8168157, "action": "move", "x": 137, "y": 268}
|
||||
{"time_stamp": 21028.8246469, "action": "move", "x": 139, "y": 268}
|
||||
{"time_stamp": 21028.8327817, "action": "move", "x": 140, "y": 268}
|
||||
{"time_stamp": 21028.8408239, "action": "move", "x": 141, "y": 268}
|
||||
{"time_stamp": 21028.8488115, "action": "move", "x": 142, "y": 267}
|
||||
{"time_stamp": 21028.8571578, "action": "move", "x": 143, "y": 267}
|
||||
{"time_stamp": 21028.8646641, "action": "move", "x": 144, "y": 267}
|
||||
{"time_stamp": 21028.8741985, "action": "move", "x": 145, "y": 267}
|
||||
{"time_stamp": 21028.8809717, "action": "move", "x": 146, "y": 267}
|
||||
{"time_stamp": 21028.8888646, "action": "move", "x": 146, "y": 267}
|
||||
{"time_stamp": 21028.961049, "action": "move", "x": 146, "y": 266}
|
||||
{"time_stamp": 21029.0249854, "action": "move", "x": 147, "y": 265}
|
||||
{"time_stamp": 21029.0328138, "action": "move", "x": 147, "y": 264}
|
||||
{"time_stamp": 21029.0407582, "action": "move", "x": 147, "y": 264}
|
||||
{"time_stamp": 21029.0487772, "action": "move", "x": 148, "y": 263}
|
||||
{"time_stamp": 21029.0569372, "action": "move", "x": 148, "y": 263}
|
||||
{"time_stamp": 21029.065073, "action": "move", "x": 149, "y": 262}
|
||||
{"time_stamp": 21029.0729933, "action": "move", "x": 150, "y": 262}
|
||||
{"time_stamp": 21029.0888149, "action": "move", "x": 150, "y": 261}
|
||||
{"time_stamp": 21029.0971595, "action": "move", "x": 151, "y": 260}
|
||||
{"time_stamp": 21029.10458, "action": "move", "x": 151, "y": 260}
|
||||
{"time_stamp": 21029.1126284, "action": "move", "x": 151, "y": 260}
|
||||
{"time_stamp": 21029.1208764, "action": "move", "x": 151, "y": 259}
|
||||
{"time_stamp": 21029.1287413, "action": "move", "x": 152, "y": 259}
|
||||
{"time_stamp": 21029.1611214, "action": "move", "x": 152, "y": 259}
|
||||
{"time_stamp": 21029.1614723, "action": "click", "x": 152, "y": 259, "button": "left", "pressed": true}
|
||||
{"time_stamp": 21029.2168134, "action": "move", "x": 152, "y": 259}
|
||||
{"time_stamp": 21029.2248681, "action": "move", "x": 154, "y": 259}
|
||||
{"time_stamp": 21029.2327317, "action": "move", "x": 156, "y": 260}
|
||||
{"time_stamp": 21029.2408222, "action": "move", "x": 158, "y": 262}
|
||||
{"time_stamp": 21029.2487515, "action": "move", "x": 163, "y": 263}
|
||||
{"time_stamp": 21029.2568152, "action": "move", "x": 169, "y": 266}
|
||||
{"time_stamp": 21029.2649126, "action": "move", "x": 174, "y": 270}
|
||||
{"time_stamp": 21029.2727425, "action": "move", "x": 183, "y": 273}
|
||||
{"time_stamp": 21029.2807226, "action": "move", "x": 190, "y": 276}
|
||||
{"time_stamp": 21029.2887741, "action": "move", "x": 200, "y": 279}
|
||||
{"time_stamp": 21029.296883, "action": "move", "x": 209, "y": 282}
|
||||
{"time_stamp": 21029.304834, "action": "move", "x": 220, "y": 285}
|
||||
{"time_stamp": 21029.3131548, "action": "move", "x": 233, "y": 287}
|
||||
{"time_stamp": 21029.3207916, "action": "move", "x": 244, "y": 290}
|
||||
{"time_stamp": 21029.3290871, "action": "move", "x": 256, "y": 292}
|
||||
{"time_stamp": 21029.3366508, "action": "move", "x": 268, "y": 293}
|
||||
{"time_stamp": 21029.3445108, "action": "move", "x": 279, "y": 294}
|
||||
{"time_stamp": 21029.3529213, "action": "move", "x": 288, "y": 297}
|
||||
{"time_stamp": 21029.3607282, "action": "move", "x": 298, "y": 297}
|
||||
{"time_stamp": 21029.3691604, "action": "move", "x": 307, "y": 297}
|
||||
{"time_stamp": 21029.3769931, "action": "move", "x": 316, "y": 298}
|
||||
{"time_stamp": 21029.3850192, "action": "move", "x": 324, "y": 300}
|
||||
{"time_stamp": 21029.3927881, "action": "move", "x": 331, "y": 301}
|
||||
{"time_stamp": 21029.4007925, "action": "move", "x": 336, "y": 302}
|
||||
{"time_stamp": 21029.4088638, "action": "move", "x": 342, "y": 304}
|
||||
{"time_stamp": 21029.4167924, "action": "move", "x": 346, "y": 304}
|
||||
{"time_stamp": 21029.4251047, "action": "move", "x": 349, "y": 304}
|
||||
{"time_stamp": 21029.4328699, "action": "move", "x": 352, "y": 306}
|
||||
{"time_stamp": 21029.4409293, "action": "move", "x": 355, "y": 306}
|
||||
{"time_stamp": 21029.4487136, "action": "move", "x": 356, "y": 307}
|
||||
{"time_stamp": 21029.4568755, "action": "move", "x": 358, "y": 308}
|
||||
{"time_stamp": 21029.4647053, "action": "move", "x": 361, "y": 309}
|
||||
{"time_stamp": 21029.4728173, "action": "move", "x": 363, "y": 310}
|
||||
{"time_stamp": 21029.4806011, "action": "move", "x": 365, "y": 311}
|
||||
{"time_stamp": 21029.4889321, "action": "move", "x": 367, "y": 312}
|
||||
{"time_stamp": 21029.4967544, "action": "move", "x": 370, "y": 313}
|
||||
{"time_stamp": 21029.5049087, "action": "move", "x": 374, "y": 314}
|
||||
{"time_stamp": 21029.5129759, "action": "move", "x": 377, "y": 316}
|
||||
{"time_stamp": 21029.5210278, "action": "move", "x": 381, "y": 317}
|
||||
{"time_stamp": 21029.5286154, "action": "move", "x": 386, "y": 317}
|
||||
{"time_stamp": 21029.5371491, "action": "move", "x": 390, "y": 318}
|
||||
{"time_stamp": 21029.5449815, "action": "move", "x": 393, "y": 319}
|
||||
{"time_stamp": 21029.5526305, "action": "move", "x": 397, "y": 319}
|
||||
{"time_stamp": 21029.5604721, "action": "move", "x": 400, "y": 319}
|
||||
{"time_stamp": 21029.5690371, "action": "move", "x": 402, "y": 319}
|
||||
{"time_stamp": 21029.5772927, "action": "move", "x": 405, "y": 319}
|
||||
{"time_stamp": 21029.5846161, "action": "move", "x": 406, "y": 319}
|
||||
{"time_stamp": 21029.5928399, "action": "move", "x": 407, "y": 319}
|
||||
{"time_stamp": 21029.6007032, "action": "move", "x": 408, "y": 319}
|
||||
{"time_stamp": 21029.609118, "action": "move", "x": 409, "y": 319}
|
||||
{"time_stamp": 21029.6166036, "action": "move", "x": 411, "y": 320}
|
||||
{"time_stamp": 21029.6249215, "action": "move", "x": 412, "y": 320}
|
||||
{"time_stamp": 21029.6327262, "action": "move", "x": 414, "y": 320}
|
||||
{"time_stamp": 21029.6408018, "action": "move", "x": 415, "y": 320}
|
||||
{"time_stamp": 21029.649463, "action": "move", "x": 418, "y": 320}
|
||||
{"time_stamp": 21029.6575693, "action": "move", "x": 420, "y": 320}
|
||||
{"time_stamp": 21029.6650956, "action": "move", "x": 423, "y": 320}
|
||||
{"time_stamp": 21029.6729346, "action": "move", "x": 426, "y": 320}
|
||||
{"time_stamp": 21029.6808747, "action": "move", "x": 429, "y": 320}
|
||||
{"time_stamp": 21029.688616, "action": "move", "x": 432, "y": 320}
|
||||
{"time_stamp": 21029.6970675, "action": "move", "x": 435, "y": 320}
|
||||
{"time_stamp": 21029.7049324, "action": "move", "x": 438, "y": 320}
|
||||
{"time_stamp": 21029.7130458, "action": "move", "x": 439, "y": 320}
|
||||
{"time_stamp": 21029.7207522, "action": "move", "x": 440, "y": 320}
|
||||
{"time_stamp": 21029.7289775, "action": "move", "x": 442, "y": 320}
|
||||
{"time_stamp": 21029.7366577, "action": "move", "x": 443, "y": 320}
|
||||
{"time_stamp": 21029.7444825, "action": "move", "x": 445, "y": 320}
|
||||
{"time_stamp": 21029.7526551, "action": "move", "x": 447, "y": 320}
|
||||
{"time_stamp": 21029.7604951, "action": "move", "x": 448, "y": 320}
|
||||
{"time_stamp": 21029.7686569, "action": "move", "x": 450, "y": 319}
|
||||
{"time_stamp": 21029.7775496, "action": "move", "x": 451, "y": 319}
|
||||
{"time_stamp": 21029.7849685, "action": "move", "x": 451, "y": 319}
|
||||
{"time_stamp": 21029.7929356, "action": "move", "x": 452, "y": 319}
|
||||
{"time_stamp": 21029.8007005, "action": "move", "x": 452, "y": 319}
|
||||
{"time_stamp": 21029.8170717, "action": "move", "x": 453, "y": 319}
|
||||
{"time_stamp": 21029.8248574, "action": "move", "x": 453, "y": 318}
|
||||
{"time_stamp": 21029.8330359, "action": "move", "x": 454, "y": 318}
|
||||
{"time_stamp": 21029.8407804, "action": "move", "x": 454, "y": 318}
|
||||
{"time_stamp": 21029.8487615, "action": "move", "x": 455, "y": 318}
|
||||
{"time_stamp": 21029.8648369, "action": "move", "x": 455, "y": 318}
|
||||
{"time_stamp": 21029.8726477, "action": "move", "x": 456, "y": 318}
|
||||
{"time_stamp": 21029.8809607, "action": "move", "x": 457, "y": 317}
|
||||
{"time_stamp": 21029.8888473, "action": "move", "x": 457, "y": 317}
|
||||
{"time_stamp": 21029.9048933, "action": "move", "x": 458, "y": 317}
|
||||
{"time_stamp": 21029.9129577, "action": "move", "x": 458, "y": 317}
|
||||
{"time_stamp": 21029.9208533, "action": "move", "x": 459, "y": 317}
|
||||
{"time_stamp": 21029.9286645, "action": "move", "x": 459, "y": 317}
|
||||
{"time_stamp": 21029.9368461, "action": "move", "x": 461, "y": 317}
|
||||
{"time_stamp": 21029.9448712, "action": "move", "x": 461, "y": 317}
|
||||
{"time_stamp": 21029.953212, "action": "move", "x": 462, "y": 317}
|
||||
{"time_stamp": 21029.9608238, "action": "move", "x": 463, "y": 317}
|
||||
{"time_stamp": 21029.9686821, "action": "move", "x": 463, "y": 317}
|
||||
{"time_stamp": 21029.9768342, "action": "move", "x": 464, "y": 317}
|
||||
{"time_stamp": 21030.361149, "action": "move", "x": 464, "y": 317}
|
||||
{"time_stamp": 21030.3613383, "action": "click", "x": 464, "y": 317, "button": "left", "pressed": false}
|
||||
{"time_stamp": 21030.9690893, "action": "move", "x": 465, "y": 317}
|
||||
{"time_stamp": 21030.9770331, "action": "move", "x": 465, "y": 317}
|
||||
{"time_stamp": 21030.9933165, "action": "move", "x": 466, "y": 317}
|
||||
{"time_stamp": 21031.8410512, "action": "press", "name": "alt_l"}
|
||||
{"time_stamp": 21032.1375784, "action": "press", "name": "="}
|
||||
{"time_stamp": 21032.2331653, "action": "release", "name": "="}
|
||||
{"time_stamp": 21032.4009051, "action": "release", "name": "alt_l"}
|
||||
{"time_stamp": 21033.1212821, "action": "move", "x": 466, "y": 317}
|
||||
{"time_stamp": 21033.1289659, "action": "move", "x": 467, "y": 320}
|
||||
{"time_stamp": 21033.1370348, "action": "move", "x": 471, "y": 325}
|
||||
{"time_stamp": 21033.1456134, "action": "move", "x": 475, "y": 332}
|
||||
{"time_stamp": 21033.1531721, "action": "move", "x": 482, "y": 340}
|
||||
{"time_stamp": 21033.1605014, "action": "move", "x": 490, "y": 349}
|
||||
{"time_stamp": 21033.1692663, "action": "move", "x": 498, "y": 359}
|
||||
{"time_stamp": 21033.1771117, "action": "move", "x": 508, "y": 371}
|
||||
{"time_stamp": 21033.1850449, "action": "move", "x": 521, "y": 383}
|
||||
{"time_stamp": 21033.1929826, "action": "move", "x": 535, "y": 399}
|
||||
{"time_stamp": 21033.201192, "action": "move", "x": 546, "y": 415}
|
||||
{"time_stamp": 21033.2089185, "action": "move", "x": 555, "y": 434}
|
||||
{"time_stamp": 21033.216848, "action": "move", "x": 563, "y": 452}
|
||||
{"time_stamp": 21033.2246769, "action": "move", "x": 570, "y": 469}
|
||||
{"time_stamp": 21033.2328685, "action": "move", "x": 574, "y": 485}
|
||||
{"time_stamp": 21033.2407514, "action": "move", "x": 577, "y": 503}
|
||||
{"time_stamp": 21033.2488102, "action": "move", "x": 578, "y": 518}
|
||||
{"time_stamp": 21033.2569003, "action": "move", "x": 578, "y": 534}
|
||||
{"time_stamp": 21033.2654896, "action": "move", "x": 580, "y": 552}
|
||||
{"time_stamp": 21033.2730147, "action": "move", "x": 580, "y": 571}
|
||||
{"time_stamp": 21033.2808888, "action": "move", "x": 582, "y": 592}
|
||||
{"time_stamp": 21033.2890461, "action": "move", "x": 583, "y": 617}
|
||||
{"time_stamp": 21033.2968868, "action": "move", "x": 586, "y": 643}
|
||||
{"time_stamp": 21033.3050093, "action": "move", "x": 588, "y": 665}
|
||||
{"time_stamp": 21033.3129685, "action": "move", "x": 591, "y": 694}
|
||||
{"time_stamp": 21033.3210515, "action": "move", "x": 592, "y": 716}
|
||||
{"time_stamp": 21033.3289082, "action": "move", "x": 594, "y": 735}
|
||||
{"time_stamp": 21033.3368274, "action": "move", "x": 598, "y": 751}
|
||||
{"time_stamp": 21033.3446464, "action": "move", "x": 601, "y": 761}
|
||||
{"time_stamp": 21033.3532343, "action": "move", "x": 604, "y": 773}
|
||||
{"time_stamp": 21033.3607161, "action": "move", "x": 606, "y": 783}
|
||||
{"time_stamp": 21033.3687129, "action": "move", "x": 608, "y": 794}
|
||||
{"time_stamp": 21033.3769088, "action": "move", "x": 611, "y": 804}
|
||||
{"time_stamp": 21033.3846615, "action": "move", "x": 614, "y": 816}
|
||||
{"time_stamp": 21033.3927661, "action": "move", "x": 617, "y": 826}
|
||||
{"time_stamp": 21033.4008999, "action": "move", "x": 619, "y": 837}
|
||||
{"time_stamp": 21033.408732, "action": "move", "x": 621, "y": 846}
|
||||
{"time_stamp": 21033.4169038, "action": "move", "x": 623, "y": 856}
|
||||
{"time_stamp": 21033.4250181, "action": "move", "x": 623, "y": 865}
|
||||
{"time_stamp": 21033.4329144, "action": "move", "x": 624, "y": 875}
|
||||
{"time_stamp": 21033.4410593, "action": "move", "x": 624, "y": 883}
|
||||
{"time_stamp": 21033.448994, "action": "move", "x": 626, "y": 891}
|
||||
{"time_stamp": 21033.4570193, "action": "move", "x": 626, "y": 899}
|
||||
{"time_stamp": 21033.4648038, "action": "move", "x": 627, "y": 906}
|
||||
{"time_stamp": 21033.4730101, "action": "move", "x": 628, "y": 913}
|
||||
{"time_stamp": 21033.4815421, "action": "move", "x": 631, "y": 920}
|
||||
{"time_stamp": 21033.4891275, "action": "move", "x": 635, "y": 926}
|
||||
{"time_stamp": 21033.4970011, "action": "move", "x": 639, "y": 930}
|
||||
{"time_stamp": 21033.5047772, "action": "move", "x": 647, "y": 935}
|
||||
{"time_stamp": 21033.5132552, "action": "move", "x": 653, "y": 939}
|
||||
{"time_stamp": 21033.5211245, "action": "move", "x": 659, "y": 943}
|
||||
{"time_stamp": 21033.5292347, "action": "move", "x": 665, "y": 947}
|
||||
{"time_stamp": 21033.5373088, "action": "move", "x": 671, "y": 950}
|
||||
{"time_stamp": 21033.5447875, "action": "move", "x": 677, "y": 955}
|
||||
{"time_stamp": 21033.5529495, "action": "move", "x": 684, "y": 960}
|
||||
{"time_stamp": 21033.5609559, "action": "move", "x": 690, "y": 965}
|
||||
{"time_stamp": 21033.5689335, "action": "move", "x": 696, "y": 971}
|
||||
{"time_stamp": 21033.5768783, "action": "move", "x": 700, "y": 977}
|
||||
{"time_stamp": 21033.5846548, "action": "move", "x": 703, "y": 981}
|
||||
{"time_stamp": 21033.5931357, "action": "move", "x": 705, "y": 985}
|
||||
{"time_stamp": 21033.6009205, "action": "move", "x": 707, "y": 988}
|
||||
{"time_stamp": 21033.6088781, "action": "move", "x": 708, "y": 991}
|
||||
{"time_stamp": 21033.6169713, "action": "move", "x": 709, "y": 994}
|
||||
{"time_stamp": 21033.6249134, "action": "move", "x": 709, "y": 997}
|
||||
{"time_stamp": 21033.6328882, "action": "move", "x": 710, "y": 999}
|
||||
{"time_stamp": 21033.6412016, "action": "move", "x": 711, "y": 1003}
|
||||
{"time_stamp": 21033.648939, "action": "move", "x": 711, "y": 1007}
|
||||
{"time_stamp": 21033.6572201, "action": "move", "x": 713, "y": 1010}
|
||||
{"time_stamp": 21033.6647348, "action": "move", "x": 715, "y": 1013}
|
||||
{"time_stamp": 21033.6730325, "action": "move", "x": 716, "y": 1017}
|
||||
{"time_stamp": 21033.6810552, "action": "move", "x": 717, "y": 1021}
|
||||
{"time_stamp": 21033.6890871, "action": "move", "x": 719, "y": 1024}
|
||||
{"time_stamp": 21033.6969594, "action": "move", "x": 720, "y": 1026}
|
||||
{"time_stamp": 21033.7048284, "action": "move", "x": 720, "y": 1028}
|
||||
{"time_stamp": 21033.7126425, "action": "move", "x": 720, "y": 1028}
|
||||
{"time_stamp": 21033.7610156, "action": "move", "x": 720, "y": 1029}
|
||||
{"time_stamp": 21033.7693689, "action": "move", "x": 720, "y": 1029}
|
||||
{"time_stamp": 21033.7772628, "action": "move", "x": 720, "y": 1030}
|
||||
{"time_stamp": 21033.7847737, "action": "move", "x": 720, "y": 1031}
|
||||
{"time_stamp": 21033.7929223, "action": "move", "x": 719, "y": 1031}
|
||||
{"time_stamp": 21033.801029, "action": "move", "x": 719, "y": 1032}
|
||||
{"time_stamp": 21033.808944, "action": "move", "x": 718, "y": 1033}
|
||||
{"time_stamp": 21033.8169394, "action": "move", "x": 717, "y": 1035}
|
||||
{"time_stamp": 21033.8248771, "action": "move", "x": 716, "y": 1035}
|
||||
{"time_stamp": 21033.8334548, "action": "move", "x": 716, "y": 1036}
|
||||
{"time_stamp": 21033.8410779, "action": "move", "x": 715, "y": 1037}
|
||||
{"time_stamp": 21033.8486117, "action": "move", "x": 715, "y": 1039}
|
||||
{"time_stamp": 21033.8568906, "action": "move", "x": 713, "y": 1039}
|
||||
{"time_stamp": 21033.8649249, "action": "move", "x": 712, "y": 1040}
|
||||
{"time_stamp": 21033.8729566, "action": "move", "x": 712, "y": 1042}
|
||||
{"time_stamp": 21033.8810286, "action": "move", "x": 711, "y": 1043}
|
||||
{"time_stamp": 21033.8888454, "action": "move", "x": 711, "y": 1044}
|
||||
{"time_stamp": 21033.8970736, "action": "move", "x": 709, "y": 1045}
|
||||
{"time_stamp": 21033.9051884, "action": "move", "x": 709, "y": 1046}
|
||||
{"time_stamp": 21033.91297, "action": "move", "x": 709, "y": 1047}
|
||||
{"time_stamp": 21033.9210518, "action": "move", "x": 709, "y": 1047}
|
||||
{"time_stamp": 21033.9770341, "action": "move", "x": 709, "y": 1047}
|
||||
{"time_stamp": 21033.9932821, "action": "move", "x": 709, "y": 1047}
|
||||
{"time_stamp": 21033.9933595, "action": "click", "x": 709, "y": 1047, "button": "left", "pressed": true}
|
||||
{"time_stamp": 21034.0734669, "action": "move", "x": 709, "y": 1047}
|
||||
{"time_stamp": 21034.0737272, "action": "click", "x": 709, "y": 1047, "button": "left", "pressed": false}
|
||||
{"time_stamp": 21034.1450402, "action": "move", "x": 709, "y": 1047}
|
||||
{"time_stamp": 21034.1608305, "action": "move", "x": 709, "y": 1047}
|
||||
{"time_stamp": 21034.1690642, "action": "move", "x": 709, "y": 1046}
|
||||
{"time_stamp": 21034.1770086, "action": "move", "x": 709, "y": 1045}
|
||||
{"time_stamp": 21034.1849649, "action": "move", "x": 709, "y": 1044}
|
||||
{"time_stamp": 21034.1927171, "action": "move", "x": 709, "y": 1043}
|
||||
{"time_stamp": 21034.2008052, "action": "move", "x": 709, "y": 1040}
|
||||
{"time_stamp": 21034.2088854, "action": "move", "x": 709, "y": 1038}
|
||||
{"time_stamp": 21034.2167939, "action": "move", "x": 709, "y": 1034}
|
||||
{"time_stamp": 21034.224882, "action": "move", "x": 709, "y": 1029}
|
||||
{"time_stamp": 21034.2327267, "action": "move", "x": 711, "y": 1023}
|
||||
{"time_stamp": 21034.2408131, "action": "move", "x": 711, "y": 1016}
|
||||
{"time_stamp": 21034.2502186, "action": "move", "x": 712, "y": 1005}
|
||||
{"time_stamp": 21034.256732, "action": "move", "x": 713, "y": 991}
|
||||
{"time_stamp": 21034.2646169, "action": "move", "x": 716, "y": 976}
|
||||
{"time_stamp": 21034.2729272, "action": "move", "x": 719, "y": 955}
|
||||
{"time_stamp": 21034.2813953, "action": "move", "x": 722, "y": 929}
|
||||
{"time_stamp": 21034.2889074, "action": "move", "x": 723, "y": 899}
|
||||
{"time_stamp": 21034.2971538, "action": "move", "x": 725, "y": 871}
|
||||
{"time_stamp": 21034.3049341, "action": "move", "x": 727, "y": 838}
|
||||
{"time_stamp": 21034.3130394, "action": "move", "x": 727, "y": 805}
|
||||
{"time_stamp": 21034.3208269, "action": "move", "x": 728, "y": 771}
|
||||
{"time_stamp": 21034.3289492, "action": "move", "x": 728, "y": 742}
|
||||
{"time_stamp": 21034.3367866, "action": "move", "x": 728, "y": 714}
|
||||
{"time_stamp": 21034.3446895, "action": "move", "x": 728, "y": 686}
|
||||
{"time_stamp": 21034.3528319, "action": "move", "x": 728, "y": 662}
|
||||
{"time_stamp": 21034.3606113, "action": "move", "x": 728, "y": 643}
|
||||
{"time_stamp": 21034.3686987, "action": "move", "x": 727, "y": 620}
|
||||
{"time_stamp": 21034.3766536, "action": "move", "x": 725, "y": 605}
|
||||
{"time_stamp": 21034.3847084, "action": "move", "x": 722, "y": 589}
|
||||
{"time_stamp": 21034.3930586, "action": "move", "x": 719, "y": 576}
|
||||
{"time_stamp": 21034.4009346, "action": "move", "x": 716, "y": 565}
|
||||
{"time_stamp": 21034.4090089, "action": "move", "x": 712, "y": 554}
|
||||
{"time_stamp": 21034.416996, "action": "move", "x": 710, "y": 544}
|
||||
{"time_stamp": 21034.4246653, "action": "move", "x": 708, "y": 536}
|
||||
{"time_stamp": 21034.4331124, "action": "move", "x": 707, "y": 527}
|
||||
{"time_stamp": 21034.4410156, "action": "move", "x": 706, "y": 519}
|
||||
{"time_stamp": 21034.4488925, "action": "move", "x": 705, "y": 509}
|
||||
{"time_stamp": 21034.4568042, "action": "move", "x": 705, "y": 500}
|
||||
{"time_stamp": 21034.4650783, "action": "move", "x": 704, "y": 492}
|
||||
{"time_stamp": 21034.472962, "action": "move", "x": 703, "y": 483}
|
||||
{"time_stamp": 21034.4809251, "action": "move", "x": 703, "y": 475}
|
||||
{"time_stamp": 21034.4889399, "action": "move", "x": 703, "y": 467}
|
||||
{"time_stamp": 21034.4968154, "action": "move", "x": 703, "y": 460}
|
||||
{"time_stamp": 21034.505111, "action": "move", "x": 703, "y": 454}
|
||||
{"time_stamp": 21034.5128327, "action": "move", "x": 703, "y": 446}
|
||||
{"time_stamp": 21034.5211697, "action": "move", "x": 704, "y": 439}
|
||||
{"time_stamp": 21034.5291453, "action": "move", "x": 704, "y": 432}
|
||||
{"time_stamp": 21034.53683, "action": "move", "x": 704, "y": 428}
|
||||
{"time_stamp": 21034.5453754, "action": "move", "x": 705, "y": 423}
|
||||
{"time_stamp": 21034.5531997, "action": "move", "x": 705, "y": 419}
|
||||
{"time_stamp": 21034.5610828, "action": "move", "x": 705, "y": 417}
|
||||
{"time_stamp": 21034.568917, "action": "move", "x": 705, "y": 414}
|
||||
{"time_stamp": 21034.5768693, "action": "move", "x": 705, "y": 412}
|
||||
{"time_stamp": 21034.5849601, "action": "move", "x": 706, "y": 409}
|
||||
{"time_stamp": 21034.5930116, "action": "move", "x": 706, "y": 406}
|
||||
{"time_stamp": 21034.6006017, "action": "move", "x": 706, "y": 404}
|
||||
{"time_stamp": 21034.6086777, "action": "move", "x": 706, "y": 402}
|
||||
{"time_stamp": 21034.6167229, "action": "move", "x": 706, "y": 400}
|
||||
{"time_stamp": 21034.6251342, "action": "move", "x": 706, "y": 398}
|
||||
{"time_stamp": 21034.6325694, "action": "move", "x": 706, "y": 396}
|
||||
{"time_stamp": 21034.6407476, "action": "move", "x": 706, "y": 393}
|
||||
{"time_stamp": 21034.6489079, "action": "move", "x": 707, "y": 390}
|
||||
{"time_stamp": 21034.6567719, "action": "move", "x": 707, "y": 388}
|
||||
{"time_stamp": 21034.6648437, "action": "move", "x": 707, "y": 386}
|
||||
{"time_stamp": 21034.6735978, "action": "move", "x": 707, "y": 383}
|
||||
{"time_stamp": 21034.6808034, "action": "move", "x": 707, "y": 381}
|
||||
{"time_stamp": 21034.6887831, "action": "move", "x": 707, "y": 379}
|
||||
{"time_stamp": 21034.6968931, "action": "move", "x": 707, "y": 377}
|
||||
{"time_stamp": 21034.7048123, "action": "move", "x": 707, "y": 375}
|
||||
{"time_stamp": 21034.7127621, "action": "move", "x": 706, "y": 373}
|
||||
{"time_stamp": 21034.7208214, "action": "move", "x": 706, "y": 372}
|
||||
{"time_stamp": 21034.7289712, "action": "move", "x": 705, "y": 371}
|
||||
{"time_stamp": 21034.7366015, "action": "move", "x": 705, "y": 370}
|
||||
{"time_stamp": 21034.7449792, "action": "move", "x": 705, "y": 369}
|
||||
{"time_stamp": 21034.7528215, "action": "move", "x": 705, "y": 368}
|
||||
{"time_stamp": 21034.7611243, "action": "move", "x": 705, "y": 367}
|
||||
{"time_stamp": 21034.7689338, "action": "move", "x": 705, "y": 366}
|
||||
{"time_stamp": 21034.7768638, "action": "move", "x": 705, "y": 365}
|
||||
{"time_stamp": 21034.7849091, "action": "move", "x": 705, "y": 364}
|
||||
{"time_stamp": 21034.792848, "action": "move", "x": 705, "y": 363}
|
||||
{"time_stamp": 21034.8010344, "action": "move", "x": 705, "y": 362}
|
||||
{"time_stamp": 21034.809155, "action": "move", "x": 704, "y": 362}
|
||||
{"time_stamp": 21034.8166183, "action": "move", "x": 704, "y": 359}
|
||||
{"time_stamp": 21034.8249556, "action": "move", "x": 704, "y": 358}
|
||||
{"time_stamp": 21034.8333238, "action": "move", "x": 704, "y": 356}
|
||||
{"time_stamp": 21034.8410045, "action": "move", "x": 703, "y": 354}
|
||||
{"time_stamp": 21034.8486685, "action": "move", "x": 703, "y": 352}
|
||||
{"time_stamp": 21034.857368, "action": "move", "x": 703, "y": 350}
|
||||
{"time_stamp": 21034.8647224, "action": "move", "x": 703, "y": 347}
|
||||
{"time_stamp": 21034.8730798, "action": "move", "x": 703, "y": 346}
|
||||
{"time_stamp": 21034.8809692, "action": "move", "x": 703, "y": 342}
|
||||
{"time_stamp": 21034.8889165, "action": "move", "x": 703, "y": 341}
|
||||
{"time_stamp": 21034.8969094, "action": "move", "x": 704, "y": 339}
|
||||
{"time_stamp": 21034.9052672, "action": "move", "x": 704, "y": 337}
|
||||
{"time_stamp": 21034.9145868, "action": "move", "x": 704, "y": 335}
|
||||
{"time_stamp": 21034.9208561, "action": "move", "x": 704, "y": 334}
|
||||
{"time_stamp": 21034.928931, "action": "move", "x": 704, "y": 333}
|
||||
{"time_stamp": 21034.9374176, "action": "move", "x": 704, "y": 332}
|
||||
{"time_stamp": 21034.9451258, "action": "move", "x": 704, "y": 330}
|
||||
{"time_stamp": 21034.9528709, "action": "move", "x": 704, "y": 329}
|
||||
{"time_stamp": 21034.9611476, "action": "move", "x": 704, "y": 328}
|
||||
{"time_stamp": 21034.968991, "action": "move", "x": 704, "y": 327}
|
||||
{"time_stamp": 21034.9768394, "action": "move", "x": 705, "y": 325}
|
||||
{"time_stamp": 21034.9848553, "action": "move", "x": 705, "y": 324}
|
||||
{"time_stamp": 21034.993121, "action": "move", "x": 705, "y": 323}
|
||||
{"time_stamp": 21035.0007992, "action": "move", "x": 706, "y": 322}
|
||||
{"time_stamp": 21035.0088762, "action": "move", "x": 707, "y": 320}
|
||||
{"time_stamp": 21035.0166123, "action": "move", "x": 707, "y": 320}
|
||||
{"time_stamp": 21035.0247724, "action": "move", "x": 708, "y": 318}
|
||||
{"time_stamp": 21035.0335071, "action": "move", "x": 708, "y": 317}
|
||||
{"time_stamp": 21035.0411458, "action": "move", "x": 709, "y": 317}
|
||||
{"time_stamp": 21035.0491997, "action": "move", "x": 709, "y": 316}
|
||||
{"time_stamp": 21035.0569637, "action": "move", "x": 711, "y": 314}
|
||||
{"time_stamp": 21035.06496, "action": "move", "x": 711, "y": 313}
|
||||
{"time_stamp": 21035.0726588, "action": "move", "x": 712, "y": 312}
|
||||
{"time_stamp": 21035.0807214, "action": "move", "x": 713, "y": 311}
|
||||
{"time_stamp": 21035.0888078, "action": "move", "x": 713, "y": 309}
|
||||
{"time_stamp": 21035.0972443, "action": "move", "x": 713, "y": 309}
|
||||
{"time_stamp": 21035.1048868, "action": "move", "x": 714, "y": 308}
|
||||
{"time_stamp": 21035.1127551, "action": "move", "x": 715, "y": 307}
|
||||
{"time_stamp": 21035.1208842, "action": "move", "x": 715, "y": 306}
|
||||
{"time_stamp": 21035.1285261, "action": "move", "x": 715, "y": 306}
|
||||
{"time_stamp": 21035.1366862, "action": "move", "x": 715, "y": 305}
|
||||
{"time_stamp": 21035.1446592, "action": "move", "x": 716, "y": 305}
|
||||
{"time_stamp": 21035.1528109, "action": "move", "x": 716, "y": 305}
|
||||
{"time_stamp": 21035.1848109, "action": "move", "x": 716, "y": 304}
|
||||
{"time_stamp": 21035.208994, "action": "move", "x": 717, "y": 304}
|
||||
{"time_stamp": 21035.2571327, "action": "move", "x": 717, "y": 304}
|
||||
{"time_stamp": 21035.2573543, "action": "click", "x": 717, "y": 304, "button": "left", "pressed": true}
|
||||
{"time_stamp": 21035.3377191, "action": "move", "x": 717, "y": 304}
|
||||
{"time_stamp": 21035.3379572, "action": "click", "x": 717, "y": 304, "button": "left", "pressed": false}
|
||||
@@ -1,34 +0,0 @@
|
||||
import cv2
|
||||
from matplotlib import pyplot as plt
|
||||
|
||||
# Load the image
|
||||
image = cv2.imread('../../mm_agents/stackoverflow.png')
|
||||
|
||||
# Convert to grayscale
|
||||
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
|
||||
|
||||
# Apply adaptive thresholding to get a binary image
|
||||
thresh = cv2.adaptiveThreshold(
|
||||
gray, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY_INV, 11, 2
|
||||
)
|
||||
|
||||
# Find contours
|
||||
contours, _ = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
|
||||
|
||||
# Filter out contours that are not of cell size
|
||||
# This is done by assuming that cells will have a relatively standard size
|
||||
# The size filter is just a placeholder, real values depend on the actual image size
|
||||
min_cell_size = 500
|
||||
max_cell_size = 5000
|
||||
cell_contours = [cnt for cnt in contours if min_cell_size < cv2.contourArea(cnt) < max_cell_size]
|
||||
|
||||
# Draw contours on the image
|
||||
contour_output = image.copy()
|
||||
cv2.drawContours(contour_output, cell_contours, -1, (0, 255, 0), 2)
|
||||
|
||||
# Display the image with cell contours
|
||||
plt.figure(figsize=(12,6))
|
||||
plt.imshow(cv2.cvtColor(contour_output, cv2.COLOR_BGR2RGB))
|
||||
plt.title('Spreadsheet with Cell Contours')
|
||||
plt.axis('off')
|
||||
plt.show()
|
||||
@@ -1,32 +0,0 @@
|
||||
from PIL import Image, ImageDraw
|
||||
|
||||
|
||||
def mark_point(image_path: str, x: int, y: int, radius: int = 5, color: str = 'red') -> str:
|
||||
"""
|
||||
Mark a point on an image and save the image.
|
||||
"""
|
||||
# Load the image
|
||||
image = Image.open(image_path)
|
||||
|
||||
# Create a draw object
|
||||
draw = ImageDraw.Draw(image)
|
||||
|
||||
# Draw a small circle to mark the point
|
||||
draw.ellipse((x - radius, y - radius, x + radius, y + radius), fill=color, outline=color)
|
||||
|
||||
# Save the image with the point marked
|
||||
marked_image_path = image_path[:-4] + '_marked' + image_path[-4:]
|
||||
image.save(marked_image_path)
|
||||
|
||||
return marked_image_path
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
image_path = 'chrome_start.png'
|
||||
x = 100
|
||||
y = 200
|
||||
radius = 30
|
||||
color = 'red'
|
||||
|
||||
marked_image_path = mark_point(image_path, x, y, radius, color)
|
||||
print(f"Marked image saved to {marked_image_path}")
|
||||
@@ -1,32 +0,0 @@
|
||||
from PIL import Image, ImageDraw
|
||||
|
||||
|
||||
def mark_point(image_path: str, x: int, y: int, radius: int = 5, color: str = 'red') -> str:
|
||||
"""
|
||||
Mark a point on an image and save the image.
|
||||
"""
|
||||
# Load the image
|
||||
image = Image.open(image_path)
|
||||
|
||||
# Create a draw object
|
||||
draw = ImageDraw.Draw(image)
|
||||
|
||||
# Draw a small circle to mark the point
|
||||
draw.ellipse((x - radius, y - radius, x + radius, y + radius), fill=color, outline=color)
|
||||
|
||||
# Save the image with the point marked
|
||||
marked_image_path = image_path[:-4] + '_marked' + image_path[-4:]
|
||||
image.save(marked_image_path)
|
||||
|
||||
return marked_image_path
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
image_path = 'chrome_start.png'
|
||||
x = 100
|
||||
y = 200
|
||||
radius = 30
|
||||
color = 'red'
|
||||
|
||||
marked_image_path = mark_point(image_path, x, y, radius, color)
|
||||
print(f"Marked image saved to {marked_image_path}")
|
||||
3144
utils/output.json
13401
utils/sample.jsonl
@@ -1,30 +0,0 @@
|
||||
{
|
||||
"root": true,
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 6,
|
||||
"sourceType": "module"
|
||||
},
|
||||
"plugins": [
|
||||
"@typescript-eslint"
|
||||
],
|
||||
"rules": {
|
||||
"@typescript-eslint/naming-convention": [
|
||||
"warn",
|
||||
{
|
||||
"selector": "import",
|
||||
"format": [ "camelCase", "PascalCase" ]
|
||||
}
|
||||
],
|
||||
"@typescript-eslint/semi": "warn",
|
||||
"curly": "warn",
|
||||
"eqeqeq": "warn",
|
||||
"no-throw-literal": "warn",
|
||||
"semi": "off"
|
||||
},
|
||||
"ignorePatterns": [
|
||||
"out",
|
||||
"dist",
|
||||
"**/*.d.ts"
|
||||
]
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
import { defineConfig } from '@vscode/test-cli';
|
||||
|
||||
export default defineConfig({
|
||||
files: 'out/test/**/*.test.js',
|
||||
});
|
||||
8
vscodeEvalExtension/.vscode/extensions.json
vendored
@@ -1,8 +0,0 @@
|
||||
{
|
||||
// See http://go.microsoft.com/fwlink/?LinkId=827846
|
||||
// for the documentation about the extensions.json format
|
||||
"recommendations": [
|
||||
"dbaeumer.vscode-eslint",
|
||||
"ms-vscode.extension-test-runner"
|
||||
]
|
||||
}
|
||||
21
vscodeEvalExtension/.vscode/launch.json
vendored
@@ -1,21 +0,0 @@
|
||||
// A launch configuration that compiles the extension and then opens it inside a new window
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Run Extension",
|
||||
"type": "extensionHost",
|
||||
"request": "launch",
|
||||
"args": [
|
||||
"--extensionDevelopmentPath=${workspaceFolder}"
|
||||
],
|
||||
"outFiles": [
|
||||
"${workspaceFolder}/out/**/*.js"
|
||||
],
|
||||
"preLaunchTask": "${defaultBuildTask}"
|
||||
}
|
||||
]
|
||||
}
|
||||