GameSentenceMiner 2.1.2.post1__py3-none-any.whl → 2.2.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- GameSentenceMiner-2.2.1.dist-info/METADATA +229 -0
- GameSentenceMiner-2.2.1.dist-info/RECORD +24 -0
- GameSentenceMiner-2.2.1.dist-info/entry_points.txt +2 -0
- GameSentenceMiner-2.2.1.dist-info/top_level.txt +1 -0
- {GameSentenceMiner → src}/anki.py +12 -15
- {GameSentenceMiner → src}/config_gui.py +3 -4
- {GameSentenceMiner → src}/configuration.py +6 -8
- src/downloader/Untitled_json.py +472 -0
- src/downloader/download_tools.py +149 -0
- {GameSentenceMiner → src}/ffmpeg.py +20 -13
- {GameSentenceMiner → src}/gametext.py +6 -6
- {GameSentenceMiner → src}/gsm.py +86 -57
- {GameSentenceMiner → src}/notification.py +5 -3
- {GameSentenceMiner → src}/obs.py +48 -16
- {GameSentenceMiner → src}/package_updater.py +1 -1
- {GameSentenceMiner → src}/util.py +11 -6
- src/vad/__init__.py +0 -0
- {GameSentenceMiner → src}/vad/silero_trim.py +2 -2
- {GameSentenceMiner → src}/vad/vosk_helper.py +2 -2
- {GameSentenceMiner → src}/vad/whisper_helper.py +2 -2
- GameSentenceMiner-2.1.2.post1.dist-info/METADATA +0 -352
- GameSentenceMiner-2.1.2.post1.dist-info/RECORD +0 -21
- GameSentenceMiner-2.1.2.post1.dist-info/entry_points.txt +0 -2
- GameSentenceMiner-2.1.2.post1.dist-info/top_level.txt +0 -1
- {GameSentenceMiner-2.1.2.post1.dist-info → GameSentenceMiner-2.2.1.dist-info}/WHEEL +0 -0
- {GameSentenceMiner → src}/__init__.py +0 -0
- {GameSentenceMiner/vad → src/downloader}/__init__.py +0 -0
- {GameSentenceMiner → src}/model.py +0 -0
@@ -0,0 +1,229 @@
|
|
1
|
+
Metadata-Version: 2.2
|
2
|
+
Name: GameSentenceMiner
|
3
|
+
Version: 2.2.1
|
4
|
+
Summary: A tool for mining sentences from games. Update: Use ffprobe from GSM Directory
|
5
|
+
Author-email: Beangate <bpwhelan95@gmail.com>
|
6
|
+
License: MIT License
|
7
|
+
Project-URL: Homepage, https://github.com/bpwhelan/GameSentenceMiner
|
8
|
+
Project-URL: Repository, https://github.com/bpwhelan/GameSentenceMiner
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
11
|
+
Classifier: Operating System :: OS Independent
|
12
|
+
Requires-Python: >=3.11
|
13
|
+
Description-Content-Type: text/markdown
|
14
|
+
Requires-Dist: requests~=2.32.3
|
15
|
+
Requires-Dist: watchdog~=5.0.2
|
16
|
+
Requires-Dist: DateTime~=5.5
|
17
|
+
Requires-Dist: pyperclip~=1.9.0
|
18
|
+
Requires-Dist: vosk~=0.3.45
|
19
|
+
Requires-Dist: soundfile~=0.12.1
|
20
|
+
Requires-Dist: toml~=0.10.2
|
21
|
+
Requires-Dist: psutil~=6.0.0
|
22
|
+
Requires-Dist: rapidfuzz~=3.9.7
|
23
|
+
Requires-Dist: obs-websocket-py~=1.0
|
24
|
+
Requires-Dist: plyer~=2.1.0
|
25
|
+
Requires-Dist: keyboard~=0.13.5
|
26
|
+
Requires-Dist: websockets~=13.0.1
|
27
|
+
Requires-Dist: stable-ts~=2.17.5
|
28
|
+
Requires-Dist: silero-vad~=5.1.2
|
29
|
+
Requires-Dist: ttkbootstrap~=1.10.1
|
30
|
+
Requires-Dist: dataclasses_json~=0.6.7
|
31
|
+
Requires-Dist: numpy
|
32
|
+
Requires-Dist: pystray
|
33
|
+
Requires-Dist: pywin32
|
34
|
+
|
35
|
+
# Game Sentence Miner
|
36
|
+
|
37
|
+
This project automates the recording of game sentence audio to help with Anki Card Creation.
|
38
|
+
|
39
|
+
This allows us to create cards from texthooker/yomitan, and automatically get screenshot and sentence audio from the
|
40
|
+
game we are playing.
|
41
|
+
|
42
|
+
Short Demo (Watch this first): https://www.youtube.com/watch?v=J2At52oWieU
|
43
|
+
|
44
|
+
Installation: https://www.youtube.com/watch?v=b-L4g9tA508
|
45
|
+
|
46
|
+
## Features:
|
47
|
+
|
48
|
+
- **OBS Replay Buffer**: Constantly records the last X seconds of gameplay.
|
49
|
+
- **Voice Activity Detection**: Automatically cuts the end of the clip to the exact moment the voice ended.
|
50
|
+
- **Clipboard Interaction**: Automatically monitors the clipboard for dialogue events.
|
51
|
+
- **Websocket Listening**: Listens to a websocket uri for text-events from stuff like Agent/Textractor.
|
52
|
+
- **Hotkey Automation**: Single hotkey to trigger video recording, screenshot, and transcription.
|
53
|
+
- **1-Click Card Creation**: Monitors anki for new cards from Yomitan, and automatically gets audio from games.
|
54
|
+
|
55
|
+
## Prerequisites
|
56
|
+
|
57
|
+
- [Python 3.11+](https://www.python.org/downloads/release/python-3119/)
|
58
|
+
- Important: 3.13 is [NOT supported](https://stackoverflow.com/questions/79175945/keyerror-version-installing-openai-whisper-on-python-3-13).
|
59
|
+
|
60
|
+
---
|
61
|
+
|
62
|
+
## 1. Installing and Running the Script
|
63
|
+
|
64
|
+
https://pypi.org/project/GameSentenceMiner/
|
65
|
+
|
66
|
+
Python + pip needs to be installed, make sure you install 3.11 or higher, since older versions may not be supported.
|
67
|
+
|
68
|
+
Install:
|
69
|
+
```commandline
|
70
|
+
pip install gamesentenceminer
|
71
|
+
```
|
72
|
+
|
73
|
+
Run:
|
74
|
+
```commandline
|
75
|
+
gamesentenceminer
|
76
|
+
```
|
77
|
+
|
78
|
+
On first run, this will download OBS and FFMPEG, and do some basic configuration. You will need to edit the GSM config for your specific Anki fields and whatnot.
|
79
|
+
|
80
|
+
---
|
81
|
+
|
82
|
+
## 2. Setting Up OBS Replay Buffer
|
83
|
+
|
84
|
+
1. Go to Settings > Output > Replay Buffer, and make sure OBS Replay Buffer is enabled, I recommend setting it to 60 seconds, but shorter and longer buffers should also work.
|
85
|
+
2. Set Scene/Source. I recommend using "Game Capture" with "Capture Audio" Enabled. And then mute Desktop/microphone
|
86
|
+
1. If "Game Capture" Does not work, use "Window Capture".
|
87
|
+
2. I recommend having a Scene PER Game, with the name of the scene labeled as the game, this makes it easier for the
|
88
|
+
script to know (with a config option) what game you are playing.
|
89
|
+
3. In Output Settings, set "Recording Format" to mkv, and "Audio Encoder" to Opus. Alternate Audio Encoder settings are supported, but will be re-encoded to Opus by default.
|
90
|
+
|
91
|
+
Here are the Settings I use in OBS. Make sure the recordings folder is the same as the "folder_to_watch" in the config.
|
92
|
+

|
93
|
+

|
94
|
+
|
95
|
+
---
|
96
|
+
|
97
|
+
## 3. Configuring the App.
|
98
|
+
|
99
|
+
### Configuration GUI
|
100
|
+
|
101
|
+
The `GameSentenceMiner` project now includes a graphical interface to simplify configuration. With default values
|
102
|
+
already set, this GUI lets you adjust settings as needed. Here’s how to get started:
|
103
|
+
|
104
|
+
#### Running the Configuration GUI
|
105
|
+
|
106
|
+
To open the GUI, you have two options:
|
107
|
+
|
108
|
+
1. **Tray Icon**: Right Click the Tray Icon and Click `Open Settings`
|
109
|
+
|
110
|
+
#### Default Settings and Customization
|
111
|
+
|
112
|
+
The GUI loads with default values for each setting, so if you’re just getting started, you may only need to change options
|
113
|
+
in the "path" config. If you make changes, remember to click **Save Settings** to apply them.
|
114
|
+
Please take a second to look through the config to see what is available, there is a lot of extra functionality hidden
|
115
|
+
behind config options.
|
116
|
+
|
117
|
+

|
118
|
+
|
119
|
+
|
120
|
+
---
|
121
|
+
|
122
|
+
|
123
|
+
## 4. One Click Card Creation
|
124
|
+
|
125
|
+
This is the flagship feature of this script, so here is a section explaining it. It is possible to do full 1-click card
|
126
|
+
creation with this tool + Yomitan/JL. The relevant settings are located in `Features` and `OBS` section in the config.
|
127
|
+
|
128
|
+
Demo: https://www.youtube.com/watch?v=9dmmXO2CGNw
|
129
|
+
|
130
|
+
Screenshots to help with setup (THIS IS ALREADY DONE FOR YOU IN NEWER VERSIONS):
|
131
|
+
|
132
|
+

|
133
|
+
|
134
|
+

|
135
|
+
|
136
|
+

|
137
|
+
|
138
|
+
## 4. Example Process
|
139
|
+
|
140
|
+
1. Start script: `gamesentenceminer`
|
141
|
+
2. Start game
|
142
|
+
3. Hook Game with Agent (or textractor) with clipboard enabled
|
143
|
+
4. Create Anki Card with target word (through a texthooker page/Yomitan)
|
144
|
+
5. When finished gaming, End script (not required)
|
145
|
+
|
146
|
+
Once the Anki card is created:
|
147
|
+
|
148
|
+
1. **OBS** will save the last X seconds of gameplay.
|
149
|
+
2. The Python script will trim the audio based on last clipboard event, and the end of voice line detected in VAD if
|
150
|
+
enabled.
|
151
|
+
3. Will attempt to update the LAST anki card created.
|
152
|
+
|
153
|
+
---
|
154
|
+
|
155
|
+
## How to Update the Script
|
156
|
+
|
157
|
+
### PIP Install (Preferred)
|
158
|
+
|
159
|
+
If you installed the script via pip, you can update it with the following command:
|
160
|
+
```bash
|
161
|
+
pip install --upgrade gamesentenceminer
|
162
|
+
```
|
163
|
+
|
164
|
+
|
165
|
+
### Source (For if you want to make edits to the script)
|
166
|
+
|
167
|
+
I will probably remove this section at a later date. If you want to make edits to the script, you should know how to do this.
|
168
|
+
|
169
|
+
To ensure you always have the latest version of this script, you can use `git pull` to update your local repository with
|
170
|
+
the latest changes from the remote repository.
|
171
|
+
|
172
|
+
#### Step-by-Step Instructions
|
173
|
+
|
174
|
+
1. Open your terminal and navigate to the directory where you cloned the repository:
|
175
|
+
```bash
|
176
|
+
cd path/to/script
|
177
|
+
```
|
178
|
+
|
179
|
+
2. Run the following command to fetch and integrate the latest changes:
|
180
|
+
```bash
|
181
|
+
git pull origin main
|
182
|
+
```
|
183
|
+
|
184
|
+
- **`origin`** refers to the remote repository from which you cloned the code.
|
185
|
+
- **`main`** refers to the main branch of the repository. I~~~~f your default branch has a different name (
|
186
|
+
e.g., `master` or `dev`), replace `main` with that branch name.
|
187
|
+
|
188
|
+
3. The `git pull` command will download and apply any updates from the remote repository to your local version.
|
189
|
+
|
190
|
+
### Example:
|
191
|
+
|
192
|
+
```bash
|
193
|
+
$ cd path/to/script
|
194
|
+
$ git pull origin master
|
195
|
+
```
|
196
|
+
|
197
|
+
---
|
198
|
+
|
199
|
+
## Disclaimer/Troubleshooting
|
200
|
+
|
201
|
+
Every game/hook is different, so it's really impossible that any script can get it perfect every time. Also OBS is
|
202
|
+
sometimes a bit finicky if running for too long. If the audio timing is off, please first try some troubleshooting
|
203
|
+
steps before making an issue:
|
204
|
+
|
205
|
+
- Try Restarting OBS
|
206
|
+
- Make sure your hook is the best you can find. (Preferably it gives you the text RIGHT when the voice line starts)
|
207
|
+
- Try Adjusting Offset Configuration in `config.toml` to better match your situation. (i.e. if the hook is late, add a
|
208
|
+
negative beginning offset)
|
209
|
+
- Try using "Trim beginning" in `VAD` settings.
|
210
|
+
|
211
|
+
### Setup Troubleshooting
|
212
|
+
|
213
|
+
Just going to continuously update this with issues that I have helped users with. Look here first if you have issues
|
214
|
+
setting it up.
|
215
|
+
|
216
|
+
- Make sure folder_to_watch is the same as your recordings path in OBS. It defaults to ~/Videos, but I recommend setting
|
217
|
+
it to ~/Videos/GSM.
|
218
|
+
- If using clipboard, make sure Agent/Textractor sending to clipboard is enabled.
|
219
|
+
- If using websocket, make sure the websocket server is running and the uri is correct in both GSM AND agent/textractor. Textractor uses a default of 6677, and I would recommend changing Agent to use 6677 as well.
|
220
|
+
|
221
|
+
|
222
|
+
## Contact
|
223
|
+
|
224
|
+
If you run into issues ask in my [Discord](https://discord.gg/yP8Qse6bb8), or make an issue here.
|
225
|
+
|
226
|
+
## Donations
|
227
|
+
|
228
|
+
If you've benefited from this or any of my other projects, please consider supporting my work
|
229
|
+
via [Github Sponsors](https://github.com/sponsors/bpwhelan) or [Ko-fi.](https://ko-fi.com/beangate)
|
@@ -0,0 +1,24 @@
|
|
1
|
+
src/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
2
|
+
src/anki.py,sha256=FfOxvozC4LBzhexiyY_uDSH3wgxzf4Uqvf8DZyyGlsQ,9138
|
3
|
+
src/config_gui.py,sha256=oY397FO5WYHluPNOtfed3Eakw3TMpVStCaoEU60jw00,48963
|
4
|
+
src/configuration.py,sha256=Cw5vUF74ZKcSinbciNMiLKpMO8M6sf-0THokHO_31nE,14010
|
5
|
+
src/ffmpeg.py,sha256=-P0ysDrBefYpu8JOV6-tu52zqg2wDMwVqSVeO5iFQxc,10836
|
6
|
+
src/gametext.py,sha256=EGFGzNrkLX_YdUWbksE7bf_4P7O-zcbdhsgubpUt5xw,4025
|
7
|
+
src/gsm.py,sha256=DTVkA-MxFq3zsxH1wl1TJPz-sQIrjZdxCtzMsE9YYW0,16250
|
8
|
+
src/model.py,sha256=oh8VVT8T1UKekbmP6MGNgQ8jIuQ_7Rg4GPzDCn2kJo8,1999
|
9
|
+
src/notification.py,sha256=4lteRvzizUb081t_87DpophOQceBnNerNq3ppq5POH8,1996
|
10
|
+
src/obs.py,sha256=SAP-_JB-37ZjZIbpWs9-MYPJHtT0BpKLD-KQuvneHtg,6538
|
11
|
+
src/package_updater.py,sha256=Yel-KWBHr5kgIJpHM1A1w5tcx_gDqrLrK0pqXRhinlI,1472
|
12
|
+
src/util.py,sha256=QRJMSEtnLaT4c2avQ0gZNn1oPtDfBbqcE8zuFHTzbd8,4569
|
13
|
+
src/downloader/Untitled_json.py,sha256=RUUl2bbbCpUDUUS0fP0tdvf5FngZ7ILdA_J5TFYAXUQ,15272
|
14
|
+
src/downloader/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
15
|
+
src/downloader/download_tools.py,sha256=m1UV8EvWJdNn2cz11BCUI2mth46RhbVdVHXYYC318V8,6378
|
16
|
+
src/vad/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
17
|
+
src/vad/silero_trim.py,sha256=rcHzSNjAm_liiKBWsdywpIC1oUrM00I7kxmomXCQ-Io,1521
|
18
|
+
src/vad/vosk_helper.py,sha256=4oaI7uesINu1sO7PA0-M4qSOmiEneRRN4Ae8ztmYCBE,5798
|
19
|
+
src/vad/whisper_helper.py,sha256=N8-FZMIjLb5CiFeyY8pz050v7RguU39B6tnvsPTbSmc,3383
|
20
|
+
GameSentenceMiner-2.2.1.dist-info/METADATA,sha256=QZzBzJ6OpsODRoGaY52g3VQOQEfhKLhrlsmNmdKpVqs,9157
|
21
|
+
GameSentenceMiner-2.2.1.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
|
22
|
+
GameSentenceMiner-2.2.1.dist-info/entry_points.txt,sha256=hzWrryQzGBdqbVqzUIBbVZvvh7MBTUYvWiIs95QrfHE,51
|
23
|
+
GameSentenceMiner-2.2.1.dist-info/top_level.txt,sha256=74rtVfumQlgAPzR5_2CgYN24MB0XARCg0t-gzk6gTrM,4
|
24
|
+
GameSentenceMiner-2.2.1.dist-info/RECORD,,
|
@@ -0,0 +1 @@
|
|
1
|
+
src
|
@@ -6,15 +6,12 @@ import urllib.request
|
|
6
6
|
|
7
7
|
import requests as req
|
8
8
|
|
9
|
-
from
|
10
|
-
from . import ffmpeg
|
11
|
-
from . import notification
|
12
|
-
from . import obs
|
9
|
+
from src import obs, util, notification, ffmpeg
|
13
10
|
|
14
|
-
from .configuration import *
|
15
|
-
from .configuration import get_config
|
16
|
-
from .gametext import get_last_two_sentences
|
17
|
-
from .obs import get_current_game
|
11
|
+
from src.configuration import *
|
12
|
+
from src.configuration import get_config
|
13
|
+
from src.gametext import get_last_two_sentences
|
14
|
+
from src.obs import get_current_game
|
18
15
|
|
19
16
|
audio_in_anki = None
|
20
17
|
screenshot_in_anki = None
|
@@ -171,15 +168,15 @@ def get_cards_by_sentence(sentence):
|
|
171
168
|
card_ids = invoke("findCards", query=query)
|
172
169
|
|
173
170
|
if not card_ids:
|
174
|
-
|
171
|
+
logger.warning(f"Didn't find any cards matching query:\n{query}")
|
175
172
|
return {}
|
176
173
|
if len(card_ids) > 1:
|
177
|
-
|
174
|
+
logger.warning(f'Found more than 1, and not updating cards for query: \n{query}')
|
178
175
|
return {}
|
179
176
|
|
180
177
|
last_notes = invoke('notesInfo', notes=[card_ids[0]])[0]
|
181
178
|
|
182
|
-
|
179
|
+
logger.info(f"Found Card to backfill!: {card_ids[0]}")
|
183
180
|
|
184
181
|
return last_notes
|
185
182
|
|
@@ -191,7 +188,7 @@ def check_for_new_cards():
|
|
191
188
|
try:
|
192
189
|
current_note_ids = get_note_ids()
|
193
190
|
except Exception as e:
|
194
|
-
|
191
|
+
logger.error(f"Error fetching Anki notes: {e}")
|
195
192
|
return
|
196
193
|
new_card_ids = current_note_ids - previous_note_ids
|
197
194
|
if new_card_ids and not first_run:
|
@@ -208,16 +205,16 @@ def update_new_card():
|
|
208
205
|
|
209
206
|
use_prev_audio = util.use_previous_audio
|
210
207
|
if util.lock.locked():
|
211
|
-
|
208
|
+
logger.info("Audio still being Trimmed, Card Queued!")
|
212
209
|
use_prev_audio = True
|
213
210
|
with util.lock:
|
214
|
-
|
211
|
+
logger.info(f"use previous audio: {use_prev_audio}")
|
215
212
|
if get_config().obs.get_game_from_scene:
|
216
213
|
obs.update_current_game()
|
217
214
|
if use_prev_audio:
|
218
215
|
update_anki_card(last_card, note=get_initial_card_info(last_card), reuse_audio=True)
|
219
216
|
else:
|
220
|
-
|
217
|
+
logger.info("New card(s) detected!")
|
221
218
|
obs.save_replay_buffer()
|
222
219
|
|
223
220
|
|
@@ -3,10 +3,9 @@ from tkinter import filedialog, messagebox, simpledialog
|
|
3
3
|
|
4
4
|
import ttkbootstrap as ttk
|
5
5
|
|
6
|
-
from .package_updater import check_for_updates, get_latest_version, update, get_current_version
|
7
|
-
from
|
8
|
-
from . import
|
9
|
-
from .configuration import *
|
6
|
+
from src.package_updater import check_for_updates, get_latest_version, update, get_current_version
|
7
|
+
from src import obs, configuration
|
8
|
+
from src.configuration import *
|
10
9
|
|
11
10
|
settings_saved = False
|
12
11
|
on_save = []
|
@@ -46,9 +46,9 @@ class General:
|
|
46
46
|
@dataclass_json
|
47
47
|
@dataclass
|
48
48
|
class Paths:
|
49
|
-
folder_to_watch: str = expanduser("~/Videos/
|
50
|
-
audio_destination: str = expanduser("~/Videos/
|
51
|
-
screenshot_destination: str = expanduser("~/Videos/
|
49
|
+
folder_to_watch: str = expanduser("~/Videos/GSM")
|
50
|
+
audio_destination: str = expanduser("~/Videos/GSM/Audio/")
|
51
|
+
screenshot_destination: str = expanduser("~/Videos/GSM/SS/")
|
52
52
|
remove_video: bool = True
|
53
53
|
remove_audio: bool = False
|
54
54
|
remove_screenshot: bool = False
|
@@ -305,13 +305,11 @@ def load_config():
|
|
305
305
|
config = ProfileConfig.from_dict(config_file)
|
306
306
|
new_config = Config(configs = {DEFAULT_CONFIG : config}, current_profile=DEFAULT_CONFIG)
|
307
307
|
|
308
|
-
print(new_config)
|
309
|
-
|
310
308
|
with open(config_path, 'w') as file:
|
311
309
|
json.dump(new_config.to_dict(), file, indent=4)
|
312
310
|
return new_config
|
313
311
|
except json.JSONDecodeError as e:
|
314
|
-
|
312
|
+
logger.error(f"Error parsing config.json: {e}")
|
315
313
|
return None
|
316
314
|
elif os.path.exists('config.toml'):
|
317
315
|
config = ProfileConfig().load_from_toml('config.toml')
|
@@ -334,7 +332,7 @@ def get_config():
|
|
334
332
|
config = config_instance.get_config()
|
335
333
|
|
336
334
|
if config.features.backfill_audio and config.features.full_auto:
|
337
|
-
|
335
|
+
logger.error("Cannot have backfill_audio and obs_full_auto_mode turned on at the same time!")
|
338
336
|
exit(1)
|
339
337
|
|
340
338
|
# print(config_instance.get_config())
|
@@ -369,7 +367,7 @@ console_handler = logging.StreamHandler()
|
|
369
367
|
console_handler.setLevel(logging.INFO)
|
370
368
|
|
371
369
|
# Create rotating file handler with level DEBUG
|
372
|
-
file_handler = RotatingFileHandler(get_log_path(), maxBytes=
|
370
|
+
file_handler = RotatingFileHandler(get_log_path(), maxBytes=10 * 1024 * 1024, backupCount=0, encoding='utf-8')
|
373
371
|
file_handler.setLevel(logging.DEBUG)
|
374
372
|
|
375
373
|
# Create a formatter
|