tumblrbot 1.4.3__tar.gz → 1.4.5__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {tumblrbot-1.4.3 → tumblrbot-1.4.5}/.gitignore +2 -1
- {tumblrbot-1.4.3 → tumblrbot-1.4.5}/PKG-INFO +57 -22
- {tumblrbot-1.4.3 → tumblrbot-1.4.5}/README.md +56 -19
- {tumblrbot-1.4.3 → tumblrbot-1.4.5}/pyproject.toml +1 -3
- {tumblrbot-1.4.3 → tumblrbot-1.4.5}/src/tumblrbot/__main__.py +5 -9
- {tumblrbot-1.4.3 → tumblrbot-1.4.5}/src/tumblrbot/flow/download.py +3 -8
- {tumblrbot-1.4.3 → tumblrbot-1.4.5}/src/tumblrbot/flow/examples.py +15 -31
- {tumblrbot-1.4.3 → tumblrbot-1.4.5}/src/tumblrbot/flow/fine_tune.py +27 -10
- {tumblrbot-1.4.3 → tumblrbot-1.4.5}/src/tumblrbot/flow/generate.py +10 -9
- {tumblrbot-1.4.3 → tumblrbot-1.4.5}/src/tumblrbot/utils/common.py +16 -4
- {tumblrbot-1.4.3 → tumblrbot-1.4.5}/src/tumblrbot/utils/config.py +2 -1
- {tumblrbot-1.4.3 → tumblrbot-1.4.5}/src/tumblrbot/utils/models.py +5 -2
- {tumblrbot-1.4.3 → tumblrbot-1.4.5}/src/tumblrbot/utils/tumblr.py +13 -15
- {tumblrbot-1.4.3 → tumblrbot-1.4.5}/.github/dependabot.yml +0 -0
- {tumblrbot-1.4.3 → tumblrbot-1.4.5}/UNLICENSE +0 -0
- {tumblrbot-1.4.3 → tumblrbot-1.4.5}/src/tumblrbot/__init__.py +0 -0
- {tumblrbot-1.4.3 → tumblrbot-1.4.5}/src/tumblrbot/flow/__init__.py +0 -0
- {tumblrbot-1.4.3 → tumblrbot-1.4.5}/src/tumblrbot/utils/__init__.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tumblrbot
|
|
3
|
-
Version: 1.4.
|
|
3
|
+
Version: 1.4.5
|
|
4
4
|
Summary: An updated bot that posts to Tumblr, based on your very own blog!
|
|
5
5
|
Requires-Python: >= 3.13
|
|
6
6
|
Description-Content-Type: text/markdown
|
|
@@ -12,35 +12,41 @@ Requires-Dist: openai
|
|
|
12
12
|
Requires-Dist: pwinput
|
|
13
13
|
Requires-Dist: pydantic
|
|
14
14
|
Requires-Dist: pydantic-settings
|
|
15
|
-
Requires-Dist: requests
|
|
16
|
-
Requires-Dist: requests-cache
|
|
17
15
|
Requires-Dist: requests-oauthlib
|
|
18
16
|
Requires-Dist: rich
|
|
19
17
|
Requires-Dist: tiktoken
|
|
20
18
|
Requires-Dist: tomlkit
|
|
21
19
|
Project-URL: Source, https://github.com/MaidThatPrograms/tumblrbot
|
|
22
20
|
|
|
21
|
+
# tumblrbot
|
|
22
|
+
|
|
23
23
|
[OAuth]: https://oauth.net/1
|
|
24
|
-
[OpenAI]: https://pypi.org/project/openai
|
|
25
24
|
[Python]: https://python.org/download
|
|
26
|
-
[Tumblr]: https://tumblr.com
|
|
27
25
|
|
|
26
|
+
[JSON Lines]: https://jsonlines.org
|
|
27
|
+
[JSON Lines Validator]: https://jsonlines.org/validator
|
|
28
|
+
|
|
29
|
+
[pip]: https://pypi.org
|
|
28
30
|
[keyring]: https://pypi.org/project/keyring
|
|
29
31
|
[Rich]: https://pypi.org/project/rich
|
|
30
32
|
|
|
33
|
+
[OpenAI]: https://pypi.org/project/openai
|
|
34
|
+
[OpenAI Pricing]: https://platform.openai.com/docs/pricing#fine-tuning
|
|
35
|
+
[OpenAI Tokens]: https://platform.openai.com/settings/organization/api-keys
|
|
36
|
+
[Fine-Tuning Portal]: https://platform.openai.com/finetune
|
|
31
37
|
[Moderation API]: https://platform.openai.com/docs/api-reference/moderations
|
|
32
|
-
|
|
38
|
+
|
|
39
|
+
[Tumblr]: https://tumblr.com
|
|
40
|
+
[Tumblr Tokens]: https://tumblr.com/oauth/apps
|
|
33
41
|
|
|
34
42
|
[Download]: src/tumblrbot/flow/download.py
|
|
35
43
|
[Examples]: src/tumblrbot/flow/examples.py
|
|
36
44
|
[Fine-Tune]: src/tumblrbot/flow/fine_tune.py
|
|
37
45
|
[Generate]: src/tumblrbot/flow/generate.py
|
|
38
46
|
[Main]: src/tumblrbot/__main__.py
|
|
39
|
-
[README.md]: README.md
|
|
40
47
|
|
|
41
|
-
[
|
|
42
|
-
|
|
43
|
-
# tumblrbot
|
|
48
|
+
[Config]: #configuration
|
|
49
|
+
[Fine-Tuning]: #manual-fine-tuning
|
|
44
50
|
[](https://python.org/pypi/tumblrbot)
|
|
45
51
|
|
|
46
52
|
Description of original project:
|
|
@@ -49,6 +55,7 @@ Description of original project:
|
|
|
49
55
|
This fork is largely a rewrite of the source code with similarities in its structure and process.
|
|
50
56
|
|
|
51
57
|
Features:
|
|
58
|
+
|
|
52
59
|
- An [interactive console][Main] for all steps of generating posts for the blog:
|
|
53
60
|
1. Asks for [OpenAI] and [Tumblr] tokens.
|
|
54
61
|
- Stores API tokens using [keyring].
|
|
@@ -75,14 +82,18 @@ Features:
|
|
|
75
82
|
- Automatically keeps the [config] file up-to-date and recreates it if missing.
|
|
76
83
|
|
|
77
84
|
**To-Do:**
|
|
85
|
+
|
|
78
86
|
- Add code documentation.
|
|
79
|
-
- Fix inaccurate post counts when downloading posts.
|
|
80
|
-
- Fix file not found error when starting fine-tuning.
|
|
81
87
|
|
|
88
|
+
**Known Issues:**
|
|
89
|
+
|
|
90
|
+
- Sometimes, you will get an error about the training file not being found when starting fine-tuning. We do not currently have a fix or workaround for this. You should instead use the online portal for fine-tuning if this continues to happen. Read more in [fine-tuning].
|
|
91
|
+
- Post counts are incorrect when downloading posts. We are not certain what the cause of this is, but our tests suggest this is a [Tumblr] API problem that is giving inaccurate numbers.
|
|
82
92
|
|
|
83
93
|
**Please submit an issue or contact us for features you want added/reimplemented.**
|
|
84
94
|
|
|
85
95
|
## Installation
|
|
96
|
+
|
|
86
97
|
1. Install the latest version of [Python]:
|
|
87
98
|
- Windows: `winget install python3`
|
|
88
99
|
- Linux (apt): `apt install python-pip`
|
|
@@ -93,17 +104,23 @@ Features:
|
|
|
93
104
|
- See [keyring] for additional requirements if you are not on Windows.
|
|
94
105
|
|
|
95
106
|
## Usage
|
|
96
|
-
|
|
107
|
+
|
|
108
|
+
Run `tumblrbot` from anywhere. Run `tumblrbot --help` for command-line options. Every command-line option corresponds to a value from the [config].
|
|
97
109
|
|
|
98
110
|
## Obtaining Tokens
|
|
111
|
+
|
|
99
112
|
### OpenAI
|
|
100
|
-
|
|
113
|
+
|
|
114
|
+
API token can be created here: [OpenAI Tokens].
|
|
115
|
+
|
|
101
116
|
1. Leave everything at the defaults and set `Project` to `Default Project`.
|
|
102
117
|
1. Press `Create secret key`.
|
|
103
118
|
1. Press `Copy` to copy the API token to your clipboard.
|
|
104
119
|
|
|
105
120
|
### Tumblr
|
|
106
|
-
|
|
121
|
+
|
|
122
|
+
API tokens can be created here: [Tumblr Tokens].
|
|
123
|
+
|
|
107
124
|
1. Press `+ Register Application`.
|
|
108
125
|
1. Enter anything for `Application Name` and `Application Description`.
|
|
109
126
|
1. Enter any URL for `Application Website` and `Default callback URL`, like `https://example.com`.
|
|
@@ -118,20 +135,38 @@ When running this program, you will be prompted to enter all of these tokens. **
|
|
|
118
135
|
After inputting the [Tumblr] tokens, you will be given a URL that you need to open in your browser. Press `Allow`, then copy and paste the URL of the page you are redirected to into the console.
|
|
119
136
|
|
|
120
137
|
## Configuration
|
|
138
|
+
|
|
121
139
|
All config options can be found in `config.toml` after running the program once. This will be kept up-to-date if there are changes to the config's format in a future update. This also means it may be worthwhile to double-check the config file after an update. Any changes to the config should be in the changelog for a given version.
|
|
122
140
|
|
|
123
141
|
All file options can include directories that will be created when the program is run.
|
|
124
142
|
|
|
125
|
-
- `custom_prompts_file`
|
|
143
|
+
- `custom_prompts_file` This file should follow the following file format:
|
|
144
|
+
|
|
126
145
|
```json
|
|
127
|
-
{"user message 1": "assistant response 1"
|
|
128
|
-
|
|
146
|
+
{"user message 1": "assistant response 1"}
|
|
147
|
+
{"user message 1": "assistant response 1"}
|
|
148
|
+
{"user message 2": "assistant response 2", "user message 3": "assistant response 3"}
|
|
129
149
|
```
|
|
150
|
+
|
|
151
|
+
To be specific, it should follow the [JSON Lines] file format with one collection of name/value pairs (a dictionary) per line. You can validate your file using the [JSON Lines Validator].
|
|
152
|
+
|
|
130
153
|
- **`developer_message`** - This message is used in for fine-tuning the AI as well as generating prompts. If you change this, you will need to run the fine-tuning again with the new value before generating posts.
|
|
131
154
|
- **`user_message`** - This message is used in the same way as `developer_message` and should be treated the same.
|
|
132
|
-
- **`expected_epochs`** - The default value here is the default number of epochs for `base_model`. You may have to change this value if you change `base_model`. After running fine-tuning once, you will see the number of epochs used in the [fine-tuning portal]
|
|
133
|
-
- **`token_price`** - The default value here is the default token price for `base_model`. You can find the up-to-date value [
|
|
134
|
-
- **`job_id`** - If there is any value here, this program will resume monitoring the corresponding job, instead of starting a new one. This gets set when starting the fine-tuning and is cleared when it is completed. You can
|
|
135
|
-
- **`base_model`** - This value is used to choose the tokenizer for estimating fine-tuning costs. It is also the base model that will be fine-tuned and the model that is used to generate tags. You can find a list of options in the [fine-tuning portal]
|
|
155
|
+
- **`expected_epochs`** - The default value here is the default number of epochs for `base_model`. You may have to change this value if you change `base_model`. After running fine-tuning once, you will see the number of epochs used in the [fine-tuning portal] under *Hyperparameters*. This value will also be updated automatically if you run fine-tuning through this program.
|
|
156
|
+
- **`token_price`** - The default value here is the default token price for `base_model`. You can find the up-to-date value in [OpenAI Pricing], in the *Training* column.
|
|
157
|
+
- **`job_id`** - If there is any value here, this program will resume monitoring the corresponding job, instead of starting a new one. This gets set when starting the fine-tuning and is cleared when it is completed. You can read more in [fine-tuning].
|
|
158
|
+
- **`base_model`** - This value is used to choose the tokenizer for estimating fine-tuning costs. It is also the base model that will be fine-tuned and the model that is used to generate tags. You can find a list of options in the [fine-tuning portal] by pressing `+ Create` and opening the drop-down list for `Base Model`. Be sure to update `token_price` if you change this value.
|
|
159
|
+
- **`fine_tuned_model`** - Set automatically after monitoring fine-tuning if the job has succeeded. You can read more in [fine-tuning].
|
|
136
160
|
- **`tags_chance`** - This should be between 0 and 1. Setting it to 0 corresponds to a 0% chance (never) to add tags to a post. 1 corresponds to a 100% chance (always) to add tags to a post. Adding tags incurs a very small token cost.
|
|
137
161
|
|
|
162
|
+
## Manual Fine-Tuning
|
|
163
|
+
|
|
164
|
+
You can manually upload the examples file to [OpenAI] and start the fine-tuning here: [fine-tuning portal].
|
|
165
|
+
|
|
166
|
+
1. Press `+ Create`.
|
|
167
|
+
1. Select the desired `Base Model` from the dropdown. This should ideally match the model set in the [config].
|
|
168
|
+
1. Upload the generated examples file to the section under `Training data`. You can find the path for this in the [config].
|
|
169
|
+
1. Press `Create`.
|
|
170
|
+
1. (Optional) Copy the value next to `Job ID` and paste it into the [config] under `job_id`. You can then run the program and monitor its progress as usual.
|
|
171
|
+
1. If you do not do the above, you will have to copy the value next to `Output model` once the job is complete and paste it into the [config] under `fine_tuned_model`.
|
|
172
|
+
|
|
@@ -1,24 +1,32 @@
|
|
|
1
|
+
# tumblrbot
|
|
2
|
+
|
|
1
3
|
[OAuth]: https://oauth.net/1
|
|
2
|
-
[OpenAI]: https://pypi.org/project/openai
|
|
3
4
|
[Python]: https://python.org/download
|
|
4
|
-
[Tumblr]: https://tumblr.com
|
|
5
5
|
|
|
6
|
+
[JSON Lines]: https://jsonlines.org
|
|
7
|
+
[JSON Lines Validator]: https://jsonlines.org/validator
|
|
8
|
+
|
|
9
|
+
[pip]: https://pypi.org
|
|
6
10
|
[keyring]: https://pypi.org/project/keyring
|
|
7
11
|
[Rich]: https://pypi.org/project/rich
|
|
8
12
|
|
|
13
|
+
[OpenAI]: https://pypi.org/project/openai
|
|
14
|
+
[OpenAI Pricing]: https://platform.openai.com/docs/pricing#fine-tuning
|
|
15
|
+
[OpenAI Tokens]: https://platform.openai.com/settings/organization/api-keys
|
|
16
|
+
[Fine-Tuning Portal]: https://platform.openai.com/finetune
|
|
9
17
|
[Moderation API]: https://platform.openai.com/docs/api-reference/moderations
|
|
10
|
-
|
|
18
|
+
|
|
19
|
+
[Tumblr]: https://tumblr.com
|
|
20
|
+
[Tumblr Tokens]: https://tumblr.com/oauth/apps
|
|
11
21
|
|
|
12
22
|
[Download]: src/tumblrbot/flow/download.py
|
|
13
23
|
[Examples]: src/tumblrbot/flow/examples.py
|
|
14
24
|
[Fine-Tune]: src/tumblrbot/flow/fine_tune.py
|
|
15
25
|
[Generate]: src/tumblrbot/flow/generate.py
|
|
16
26
|
[Main]: src/tumblrbot/__main__.py
|
|
17
|
-
[README.md]: README.md
|
|
18
27
|
|
|
19
|
-
[
|
|
20
|
-
|
|
21
|
-
# tumblrbot
|
|
28
|
+
[Config]: #configuration
|
|
29
|
+
[Fine-Tuning]: #manual-fine-tuning
|
|
22
30
|
[](https://python.org/pypi/tumblrbot)
|
|
23
31
|
|
|
24
32
|
Description of original project:
|
|
@@ -27,6 +35,7 @@ Description of original project:
|
|
|
27
35
|
This fork is largely a rewrite of the source code with similarities in its structure and process.
|
|
28
36
|
|
|
29
37
|
Features:
|
|
38
|
+
|
|
30
39
|
- An [interactive console][Main] for all steps of generating posts for the blog:
|
|
31
40
|
1. Asks for [OpenAI] and [Tumblr] tokens.
|
|
32
41
|
- Stores API tokens using [keyring].
|
|
@@ -53,14 +62,18 @@ Features:
|
|
|
53
62
|
- Automatically keeps the [config] file up-to-date and recreates it if missing.
|
|
54
63
|
|
|
55
64
|
**To-Do:**
|
|
65
|
+
|
|
56
66
|
- Add code documentation.
|
|
57
|
-
- Fix inaccurate post counts when downloading posts.
|
|
58
|
-
- Fix file not found error when starting fine-tuning.
|
|
59
67
|
|
|
68
|
+
**Known Issues:**
|
|
69
|
+
|
|
70
|
+
- Sometimes, you will get an error about the training file not being found when starting fine-tuning. We do not currently have a fix or workaround for this. You should instead use the online portal for fine-tuning if this continues to happen. Read more in [fine-tuning].
|
|
71
|
+
- Post counts are incorrect when downloading posts. We are not certain what the cause of this is, but our tests suggest this is a [Tumblr] API problem that is giving inaccurate numbers.
|
|
60
72
|
|
|
61
73
|
**Please submit an issue or contact us for features you want added/reimplemented.**
|
|
62
74
|
|
|
63
75
|
## Installation
|
|
76
|
+
|
|
64
77
|
1. Install the latest version of [Python]:
|
|
65
78
|
- Windows: `winget install python3`
|
|
66
79
|
- Linux (apt): `apt install python-pip`
|
|
@@ -71,17 +84,23 @@ Features:
|
|
|
71
84
|
- See [keyring] for additional requirements if you are not on Windows.
|
|
72
85
|
|
|
73
86
|
## Usage
|
|
74
|
-
|
|
87
|
+
|
|
88
|
+
Run `tumblrbot` from anywhere. Run `tumblrbot --help` for command-line options. Every command-line option corresponds to a value from the [config].
|
|
75
89
|
|
|
76
90
|
## Obtaining Tokens
|
|
91
|
+
|
|
77
92
|
### OpenAI
|
|
78
|
-
|
|
93
|
+
|
|
94
|
+
API token can be created here: [OpenAI Tokens].
|
|
95
|
+
|
|
79
96
|
1. Leave everything at the defaults and set `Project` to `Default Project`.
|
|
80
97
|
1. Press `Create secret key`.
|
|
81
98
|
1. Press `Copy` to copy the API token to your clipboard.
|
|
82
99
|
|
|
83
100
|
### Tumblr
|
|
84
|
-
|
|
101
|
+
|
|
102
|
+
API tokens can be created here: [Tumblr Tokens].
|
|
103
|
+
|
|
85
104
|
1. Press `+ Register Application`.
|
|
86
105
|
1. Enter anything for `Application Name` and `Application Description`.
|
|
87
106
|
1. Enter any URL for `Application Website` and `Default callback URL`, like `https://example.com`.
|
|
@@ -96,19 +115,37 @@ When running this program, you will be prompted to enter all of these tokens. **
|
|
|
96
115
|
After inputting the [Tumblr] tokens, you will be given a URL that you need to open in your browser. Press `Allow`, then copy and paste the URL of the page you are redirected to into the console.
|
|
97
116
|
|
|
98
117
|
## Configuration
|
|
118
|
+
|
|
99
119
|
All config options can be found in `config.toml` after running the program once. This will be kept up-to-date if there are changes to the config's format in a future update. This also means it may be worthwhile to double-check the config file after an update. Any changes to the config should be in the changelog for a given version.
|
|
100
120
|
|
|
101
121
|
All file options can include directories that will be created when the program is run.
|
|
102
122
|
|
|
103
|
-
- `custom_prompts_file`
|
|
123
|
+
- `custom_prompts_file` This file should follow the following file format:
|
|
124
|
+
|
|
104
125
|
```json
|
|
105
|
-
{"user message 1": "assistant response 1"
|
|
106
|
-
|
|
126
|
+
{"user message 1": "assistant response 1"}
|
|
127
|
+
{"user message 1": "assistant response 1"}
|
|
128
|
+
{"user message 2": "assistant response 2", "user message 3": "assistant response 3"}
|
|
107
129
|
```
|
|
130
|
+
|
|
131
|
+
To be specific, it should follow the [JSON Lines] file format with one collection of name/value pairs (a dictionary) per line. You can validate your file using the [JSON Lines Validator].
|
|
132
|
+
|
|
108
133
|
- **`developer_message`** - This message is used in for fine-tuning the AI as well as generating prompts. If you change this, you will need to run the fine-tuning again with the new value before generating posts.
|
|
109
134
|
- **`user_message`** - This message is used in the same way as `developer_message` and should be treated the same.
|
|
110
|
-
- **`expected_epochs`** - The default value here is the default number of epochs for `base_model`. You may have to change this value if you change `base_model`. After running fine-tuning once, you will see the number of epochs used in the [fine-tuning portal]
|
|
111
|
-
- **`token_price`** - The default value here is the default token price for `base_model`. You can find the up-to-date value [
|
|
112
|
-
- **`job_id`** - If there is any value here, this program will resume monitoring the corresponding job, instead of starting a new one. This gets set when starting the fine-tuning and is cleared when it is completed. You can
|
|
113
|
-
- **`base_model`** - This value is used to choose the tokenizer for estimating fine-tuning costs. It is also the base model that will be fine-tuned and the model that is used to generate tags. You can find a list of options in the [fine-tuning portal]
|
|
135
|
+
- **`expected_epochs`** - The default value here is the default number of epochs for `base_model`. You may have to change this value if you change `base_model`. After running fine-tuning once, you will see the number of epochs used in the [fine-tuning portal] under *Hyperparameters*. This value will also be updated automatically if you run fine-tuning through this program.
|
|
136
|
+
- **`token_price`** - The default value here is the default token price for `base_model`. You can find the up-to-date value in [OpenAI Pricing], in the *Training* column.
|
|
137
|
+
- **`job_id`** - If there is any value here, this program will resume monitoring the corresponding job, instead of starting a new one. This gets set when starting the fine-tuning and is cleared when it is completed. You can read more in [fine-tuning].
|
|
138
|
+
- **`base_model`** - This value is used to choose the tokenizer for estimating fine-tuning costs. It is also the base model that will be fine-tuned and the model that is used to generate tags. You can find a list of options in the [fine-tuning portal] by pressing `+ Create` and opening the drop-down list for `Base Model`. Be sure to update `token_price` if you change this value.
|
|
139
|
+
- **`fine_tuned_model`** - Set automatically after monitoring fine-tuning if the job has succeeded. You can read more in [fine-tuning].
|
|
114
140
|
- **`tags_chance`** - This should be between 0 and 1. Setting it to 0 corresponds to a 0% chance (never) to add tags to a post. 1 corresponds to a 100% chance (always) to add tags to a post. Adding tags incurs a very small token cost.
|
|
141
|
+
|
|
142
|
+
## Manual Fine-Tuning
|
|
143
|
+
|
|
144
|
+
You can manually upload the examples file to [OpenAI] and start the fine-tuning here: [fine-tuning portal].
|
|
145
|
+
|
|
146
|
+
1. Press `+ Create`.
|
|
147
|
+
1. Select the desired `Base Model` from the dropdown. This should ideally match the model set in the [config].
|
|
148
|
+
1. Upload the generated examples file to the section under `Training data`. You can find the path for this in the [config].
|
|
149
|
+
1. Press `Create`.
|
|
150
|
+
1. (Optional) Copy the value next to `Job ID` and paste it into the [config] under `job_id`. You can then run the program and monitor its progress as usual.
|
|
151
|
+
1. If you do not do the above, you will have to copy the value next to `Output model` once the job is complete and paste it into the [config] under `fine_tuned_model`.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "tumblrbot"
|
|
3
|
-
version = "1.4.
|
|
3
|
+
version = "1.4.5"
|
|
4
4
|
description = "An updated bot that posts to Tumblr, based on your very own blog!"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">= 3.13"
|
|
@@ -13,8 +13,6 @@ dependencies = [
|
|
|
13
13
|
"pwinput",
|
|
14
14
|
"pydantic",
|
|
15
15
|
"pydantic-settings",
|
|
16
|
-
"requests",
|
|
17
|
-
"requests-cache",
|
|
18
16
|
"requests-oauthlib",
|
|
19
17
|
"rich",
|
|
20
18
|
"tiktoken",
|
|
@@ -19,22 +19,18 @@ def main() -> None:
|
|
|
19
19
|
OpenAI(api_key=tokens.openai_api_key.get_secret_value(), http_client=DefaultHttpxClient(http2=True)) as openai,
|
|
20
20
|
TumblrSession(tokens=tokens) as tumblr,
|
|
21
21
|
):
|
|
22
|
-
post_downloader = PostDownloader(openai, tumblr)
|
|
23
22
|
if Confirm.ask("Download latest posts?", default=False):
|
|
24
|
-
|
|
25
|
-
download_paths = post_downloader.get_data_paths()
|
|
23
|
+
PostDownloader(openai=openai, tumblr=tumblr).main()
|
|
26
24
|
|
|
27
|
-
examples_writer = ExamplesWriter(openai, tumblr, download_paths)
|
|
28
25
|
if Confirm.ask("Create training data?", default=False):
|
|
29
|
-
|
|
30
|
-
estimated_tokens = sum(examples_writer.count_tokens())
|
|
26
|
+
ExamplesWriter(openai=openai, tumblr=tumblr).main()
|
|
31
27
|
|
|
32
|
-
fine_tuner = FineTuner(openai, tumblr
|
|
28
|
+
fine_tuner = FineTuner(openai=openai, tumblr=tumblr)
|
|
33
29
|
fine_tuner.print_estimates()
|
|
34
30
|
|
|
35
31
|
message = "Resume monitoring the previous fine-tuning process?" if FlowClass.config.job_id else "Upload data to OpenAI for fine-tuning?"
|
|
36
32
|
if Confirm.ask(f"{message} [bold]You must do this to set the model to generate drafts from. Alternatively, manually enter a model into the config", default=False):
|
|
37
|
-
fine_tuner.
|
|
33
|
+
fine_tuner.main()
|
|
38
34
|
|
|
39
35
|
if Confirm.ask("Generate drafts?", default=False):
|
|
40
|
-
DraftGenerator(openai, tumblr).
|
|
36
|
+
DraftGenerator(openai=openai, tumblr=tumblr).main()
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
from io import TextIOBase
|
|
2
2
|
from json import dump
|
|
3
|
-
from
|
|
3
|
+
from typing import override
|
|
4
4
|
|
|
5
5
|
from tumblrbot.utils.common import FlowClass, PreviewLive
|
|
6
6
|
from tumblrbot.utils.models import Post
|
|
7
7
|
|
|
8
8
|
|
|
9
9
|
class PostDownloader(FlowClass):
|
|
10
|
-
|
|
10
|
+
@override
|
|
11
|
+
def main(self) -> None:
|
|
11
12
|
self.config.data_directory.mkdir(parents=True, exist_ok=True)
|
|
12
13
|
|
|
13
14
|
with PreviewLive() as live:
|
|
@@ -50,9 +51,3 @@ class PostDownloader(FlowClass):
|
|
|
50
51
|
completed += len(posts)
|
|
51
52
|
else:
|
|
52
53
|
return
|
|
53
|
-
|
|
54
|
-
def get_data_paths(self) -> list[Path]:
|
|
55
|
-
return list(map(self.get_data_path, self.config.download_blog_identifiers))
|
|
56
|
-
|
|
57
|
-
def get_data_path(self, blog_identifier: str) -> Path:
|
|
58
|
-
return (self.config.data_directory / blog_identifier).with_suffix(".jsonl")
|
|
@@ -1,27 +1,21 @@
|
|
|
1
1
|
from collections.abc import Generator
|
|
2
|
-
from dataclasses import dataclass
|
|
3
2
|
from json import loads
|
|
4
3
|
from math import ceil
|
|
5
|
-
from pathlib import Path
|
|
6
4
|
from re import search
|
|
7
|
-
from typing import IO
|
|
5
|
+
from typing import IO, override
|
|
8
6
|
|
|
9
7
|
import rich
|
|
10
8
|
from more_itertools import chunked
|
|
11
9
|
from openai import BadRequestError
|
|
12
|
-
from rich.console import Console
|
|
13
10
|
from rich.prompt import Confirm
|
|
14
|
-
from tiktoken import encoding_for_model, get_encoding
|
|
15
11
|
|
|
16
12
|
from tumblrbot.utils.common import FlowClass, PreviewLive
|
|
17
13
|
from tumblrbot.utils.models import Example, Post
|
|
18
14
|
|
|
19
15
|
|
|
20
|
-
@dataclass
|
|
21
16
|
class ExamplesWriter(FlowClass):
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
def write_examples(self) -> None:
|
|
17
|
+
@override
|
|
18
|
+
def main(self) -> None:
|
|
25
19
|
self.config.examples_file.parent.mkdir(parents=True, exist_ok=True)
|
|
26
20
|
|
|
27
21
|
with self.config.examples_file.open("w", encoding="utf_8") as fp:
|
|
@@ -52,16 +46,22 @@ class ExamplesWriter(FlowClass):
|
|
|
52
46
|
fp.write(f"{example.model_dump_json()}\n")
|
|
53
47
|
|
|
54
48
|
def get_custom_prompts(self) -> Generator[tuple[str, str]]:
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
49
|
+
self.config.custom_prompts_file.parent.mkdir(parents=True, exist_ok=True)
|
|
50
|
+
self.config.custom_prompts_file.touch(exist_ok=True)
|
|
51
|
+
|
|
52
|
+
with self.config.custom_prompts_file.open("r", encoding="utf_8") as fp:
|
|
53
|
+
for line in fp:
|
|
54
|
+
data: dict[str, str] = loads(line)
|
|
55
|
+
yield from data.items()
|
|
58
56
|
|
|
59
57
|
def get_filtered_posts(self) -> Generator[Post]:
|
|
60
|
-
posts =
|
|
58
|
+
posts = self.get_valid_posts()
|
|
61
59
|
|
|
62
60
|
if Confirm.ask("[gray62]Remove posts flagged by the OpenAI moderation? This can sometimes resolve errors with fine-tuning validation, but is slow.", default=False):
|
|
63
|
-
removed = 0
|
|
64
61
|
chunk_size = self.get_moderation_chunk_limit()
|
|
62
|
+
posts = list(posts)
|
|
63
|
+
removed = 0
|
|
64
|
+
|
|
65
65
|
with PreviewLive() as live:
|
|
66
66
|
for chunk in live.progress.track(
|
|
67
67
|
chunked(posts, chunk_size),
|
|
@@ -80,7 +80,7 @@ class ExamplesWriter(FlowClass):
|
|
|
80
80
|
yield from posts
|
|
81
81
|
|
|
82
82
|
def get_valid_posts(self) -> Generator[Post]:
|
|
83
|
-
for data_path in self.
|
|
83
|
+
for data_path in self.get_data_paths():
|
|
84
84
|
with data_path.open(encoding="utf_8") as fp:
|
|
85
85
|
for line in fp:
|
|
86
86
|
post = Post.model_validate_json(line)
|
|
@@ -96,19 +96,3 @@ class ExamplesWriter(FlowClass):
|
|
|
96
96
|
if match := search(r"(\d+)\.", message):
|
|
97
97
|
return int(match.group(1))
|
|
98
98
|
return test_n
|
|
99
|
-
|
|
100
|
-
def count_tokens(self) -> Generator[int]:
|
|
101
|
-
# Based on https://cookbook.openai.com/examples/how_to_count_tokens_with_tiktoken
|
|
102
|
-
# and https://cookbook.openai.com/examples/chat_finetuning_data_prep
|
|
103
|
-
try:
|
|
104
|
-
encoding = encoding_for_model(self.config.base_model)
|
|
105
|
-
except KeyError as error:
|
|
106
|
-
encoding = get_encoding("o200k_base")
|
|
107
|
-
Console(stderr=True, style="logging.level.warning").print(f"[Warning] Using encoding '{encoding.name}': {''.join(error.args)}\n")
|
|
108
|
-
|
|
109
|
-
with self.config.examples_file.open(encoding="utf_8") as fp:
|
|
110
|
-
for line in fp:
|
|
111
|
-
example = Example.model_validate_json(line)
|
|
112
|
-
yield len(encoding.encode("assistant")) # every reply is primed with <|start|>assistant<|message|>
|
|
113
|
-
for message in example.messages:
|
|
114
|
-
yield 4 + len(encoding.encode(message.content))
|
|
@@ -1,25 +1,27 @@
|
|
|
1
|
-
from
|
|
1
|
+
from collections.abc import Generator
|
|
2
2
|
from datetime import datetime
|
|
3
3
|
from textwrap import dedent
|
|
4
|
-
from time import sleep
|
|
4
|
+
from time import sleep
|
|
5
|
+
from typing import override
|
|
5
6
|
|
|
6
7
|
import rich
|
|
7
8
|
from openai.types.fine_tuning import FineTuningJob
|
|
8
9
|
from rich import progress
|
|
10
|
+
from rich.console import Console
|
|
9
11
|
from rich.prompt import Confirm
|
|
12
|
+
from tiktoken import encoding_for_model, get_encoding
|
|
10
13
|
|
|
11
14
|
from tumblrbot.utils.common import FlowClass, PreviewLive
|
|
15
|
+
from tumblrbot.utils.models import Example
|
|
12
16
|
|
|
13
17
|
|
|
14
|
-
@dataclass
|
|
15
18
|
class FineTuner(FlowClass):
|
|
16
|
-
estimated_tokens: int
|
|
17
|
-
|
|
18
19
|
@staticmethod
|
|
19
20
|
def dedent_print(text: str) -> None:
|
|
20
21
|
rich.print(dedent(text).lstrip())
|
|
21
22
|
|
|
22
|
-
|
|
23
|
+
@override
|
|
24
|
+
def main(self) -> None:
|
|
23
25
|
job = self.create_job()
|
|
24
26
|
|
|
25
27
|
self.dedent_print(f"""
|
|
@@ -39,8 +41,6 @@ class FineTuner(FlowClass):
|
|
|
39
41
|
|
|
40
42
|
live.progress.update(
|
|
41
43
|
task_id,
|
|
42
|
-
total=job.estimated_finish - job.created_at if job.estimated_finish else None,
|
|
43
|
-
completed=time() - job.created_at,
|
|
44
44
|
description=f"Fine-tuning: [italic]{job.status.replace('_', ' ').title()}[/]...",
|
|
45
45
|
)
|
|
46
46
|
|
|
@@ -102,16 +102,33 @@ class FineTuner(FlowClass):
|
|
|
102
102
|
self.config.fine_tuned_model = job.fine_tuned_model or ""
|
|
103
103
|
|
|
104
104
|
def print_estimates(self) -> None:
|
|
105
|
-
|
|
105
|
+
estimated_tokens = sum(self.count_tokens())
|
|
106
|
+
total_tokens = self.config.expected_epochs * estimated_tokens
|
|
106
107
|
cost_string = self.get_cost_string(total_tokens)
|
|
107
108
|
|
|
108
109
|
self.dedent_print(f"""
|
|
109
|
-
Tokens {
|
|
110
|
+
Tokens {estimated_tokens:,}:
|
|
110
111
|
Total tokens for [bold orange1]{self.config.expected_epochs}[/] epoch(s): {total_tokens:,}
|
|
111
112
|
Expected cost when trained with [bold purple]{self.config.base_model}[/]: {cost_string}
|
|
112
113
|
NOTE: Token values are approximate and may not be 100% accurate, please be aware of this when using the data.
|
|
113
114
|
[italic red]Amelia, Mutsumi, and Marin are not responsible for any inaccuracies in the token count or estimated price.[/]
|
|
114
115
|
""")
|
|
115
116
|
|
|
117
|
+
def count_tokens(self) -> Generator[int]:
|
|
118
|
+
# Based on https://cookbook.openai.com/examples/how_to_count_tokens_with_tiktoken
|
|
119
|
+
# and https://cookbook.openai.com/examples/chat_finetuning_data_prep
|
|
120
|
+
try:
|
|
121
|
+
encoding = encoding_for_model(self.config.base_model)
|
|
122
|
+
except KeyError as error:
|
|
123
|
+
encoding = get_encoding("o200k_base")
|
|
124
|
+
Console(stderr=True, style="logging.level.warning").print(f"[Warning] Using encoding '{encoding.name}': {''.join(error.args)}\n")
|
|
125
|
+
|
|
126
|
+
with self.config.examples_file.open(encoding="utf_8") as fp:
|
|
127
|
+
for line in fp:
|
|
128
|
+
example = Example.model_validate_json(line)
|
|
129
|
+
yield len(encoding.encode("assistant")) # every reply is primed with <|start|>assistant<|message|>
|
|
130
|
+
for message in example.messages:
|
|
131
|
+
yield 4 + len(encoding.encode(message.content))
|
|
132
|
+
|
|
116
133
|
def get_cost_string(self, total_tokens: int) -> str:
|
|
117
134
|
return f"${self.config.token_price / 1000000 * total_tokens:.2f}"
|
|
@@ -1,13 +1,18 @@
|
|
|
1
1
|
from random import random
|
|
2
|
+
from typing import override
|
|
2
3
|
|
|
3
4
|
import rich
|
|
5
|
+
from rich.prompt import IntPrompt
|
|
4
6
|
|
|
5
7
|
from tumblrbot.utils.common import FlowClass, PreviewLive
|
|
6
8
|
from tumblrbot.utils.models import Post
|
|
7
9
|
|
|
8
10
|
|
|
9
11
|
class DraftGenerator(FlowClass):
|
|
10
|
-
|
|
12
|
+
@override
|
|
13
|
+
def main(self) -> None:
|
|
14
|
+
self.config.draft_count = IntPrompt.ask("How many drafts should be generated?", default=self.config.draft_count)
|
|
15
|
+
|
|
11
16
|
message = f"View drafts here: https://tumblr.com/blog/{self.config.upload_blog_identifier}/drafts"
|
|
12
17
|
|
|
13
18
|
with PreviewLive() as live:
|
|
@@ -24,10 +29,7 @@ class DraftGenerator(FlowClass):
|
|
|
24
29
|
|
|
25
30
|
def generate_post(self) -> Post:
|
|
26
31
|
content = self.generate_content()
|
|
27
|
-
post = Post(
|
|
28
|
-
content=[content],
|
|
29
|
-
state="draft",
|
|
30
|
-
)
|
|
32
|
+
post = Post(content=[content])
|
|
31
33
|
if tags := self.generate_tags(content):
|
|
32
34
|
post.tags = tags.tags
|
|
33
35
|
return post
|
|
@@ -39,16 +41,15 @@ class DraftGenerator(FlowClass):
|
|
|
39
41
|
model=self.config.fine_tuned_model,
|
|
40
42
|
).output_text
|
|
41
43
|
|
|
42
|
-
return Post.Block(
|
|
44
|
+
return Post.Block(text=content)
|
|
43
45
|
|
|
44
46
|
def generate_tags(self, content: Post.Block) -> Post | None:
|
|
45
47
|
if random() < self.config.tags_chance: # noqa: S311
|
|
46
48
|
return self.openai.responses.parse(
|
|
47
49
|
text_format=Post,
|
|
48
|
-
input=
|
|
49
|
-
instructions=
|
|
50
|
+
input=content.text,
|
|
51
|
+
instructions=self.config.tags_developer_message,
|
|
50
52
|
model=self.config.base_model,
|
|
51
|
-
temperature=0.5,
|
|
52
53
|
).output_parsed
|
|
53
54
|
|
|
54
55
|
return None
|
|
@@ -1,25 +1,37 @@
|
|
|
1
|
-
from
|
|
1
|
+
from abc import abstractmethod
|
|
2
2
|
from random import choice
|
|
3
3
|
from typing import ClassVar, Self, override
|
|
4
4
|
|
|
5
5
|
from openai import OpenAI
|
|
6
|
+
from pydantic import ConfigDict
|
|
6
7
|
from rich._spinners import SPINNERS
|
|
7
8
|
from rich.console import RenderableType
|
|
8
9
|
from rich.live import Live
|
|
9
10
|
from rich.progress import MofNCompleteColumn, Progress, SpinnerColumn, TimeElapsedColumn
|
|
10
11
|
from rich.table import Table
|
|
11
12
|
|
|
12
|
-
from tumblrbot.utils.config import Config
|
|
13
|
+
from tumblrbot.utils.config import Config, Path
|
|
14
|
+
from tumblrbot.utils.models import FullyValidatedModel
|
|
13
15
|
from tumblrbot.utils.tumblr import TumblrSession
|
|
14
16
|
|
|
15
17
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
+
class FlowClass(FullyValidatedModel):
|
|
19
|
+
model_config = ConfigDict(arbitrary_types_allowed=True)
|
|
20
|
+
|
|
18
21
|
config: ClassVar = Config() # pyright: ignore[reportCallIssue]
|
|
19
22
|
|
|
20
23
|
openai: OpenAI
|
|
21
24
|
tumblr: TumblrSession
|
|
22
25
|
|
|
26
|
+
@abstractmethod
|
|
27
|
+
def main(self) -> None: ...
|
|
28
|
+
|
|
29
|
+
def get_data_paths(self) -> list[Path]:
|
|
30
|
+
return list(map(self.get_data_path, self.config.download_blog_identifiers))
|
|
31
|
+
|
|
32
|
+
def get_data_path(self, blog_identifier: str) -> Path:
|
|
33
|
+
return (self.config.data_directory / blog_identifier).with_suffix(".jsonl")
|
|
34
|
+
|
|
23
35
|
|
|
24
36
|
class PreviewLive(Live):
|
|
25
37
|
def __init__(self) -> None:
|
|
@@ -31,7 +31,7 @@ class Config(BaseSettings):
|
|
|
31
31
|
data_directory: Path = Field(Path("data"), description="Where to store downloaded post data.")
|
|
32
32
|
|
|
33
33
|
# Writing Examples
|
|
34
|
-
custom_prompts_file: Path = Field(Path("custom_prompts.
|
|
34
|
+
custom_prompts_file: Path = Field(Path("custom_prompts.jsonl"), description="Where to read in custom prompts from.")
|
|
35
35
|
|
|
36
36
|
# Writing Examples & Fine-Tuning
|
|
37
37
|
examples_file: Path = Field(Path("examples.jsonl"), description="Where to output the examples that will be used to fine-tune the model.")
|
|
@@ -53,6 +53,7 @@ class Config(BaseSettings):
|
|
|
53
53
|
upload_blog_identifier: str = Field("", description="The identifier of the blog which generated drafts will be uploaded to. This must be a blog associated with the same account as the configured Tumblr secret tokens.")
|
|
54
54
|
draft_count: PositiveInt = Field(150, description="The number of drafts to process. This will affect the number of tokens used with OpenAI")
|
|
55
55
|
tags_chance: NonNegativeFloat = Field(0.1, description="The chance to generate tags for any given post. This will incur extra calls to OpenAI.")
|
|
56
|
+
tags_developer_message: str = Field("You will be provided with a block of text, and your task is to extract a very short list of the most important subjects from it.", description="The developer message used to generate tags.")
|
|
56
57
|
|
|
57
58
|
@override
|
|
58
59
|
@classmethod
|
|
@@ -3,6 +3,7 @@ from typing import Annotated, Any, ClassVar, Literal, Self, override
|
|
|
3
3
|
|
|
4
4
|
import rich
|
|
5
5
|
from keyring import get_password, set_password
|
|
6
|
+
from niquests import Session
|
|
6
7
|
from openai import BaseModel
|
|
7
8
|
from pwinput import pwinput
|
|
8
9
|
from pydantic import ConfigDict, PlainSerializer, SecretStr
|
|
@@ -69,6 +70,8 @@ class Tokens(FullyValidatedModel):
|
|
|
69
70
|
if not all(self.tumblr.model_dump(mode="json").values()) or Confirm.ask("Reset Tumblr API tokens?", default=False):
|
|
70
71
|
self.tumblr.client_key, self.tumblr.client_secret = self.online_token_prompt("https://tumblr.com/oauth/apps", "consumer key", "consumer secret")
|
|
71
72
|
|
|
73
|
+
OAuth1Session.__bases__ = (Session,)
|
|
74
|
+
|
|
72
75
|
with OAuth1Session(
|
|
73
76
|
self.tumblr.client_key.get_secret_value(),
|
|
74
77
|
self.tumblr.client_secret.get_secret_value(),
|
|
@@ -95,13 +98,13 @@ class Tokens(FullyValidatedModel):
|
|
|
95
98
|
|
|
96
99
|
class Post(FullyValidatedModel):
|
|
97
100
|
class Block(FullyValidatedModel):
|
|
98
|
-
type: str = ""
|
|
101
|
+
type: str = "text"
|
|
99
102
|
text: str = ""
|
|
100
103
|
blocks: list[int] = [] # noqa: RUF012
|
|
101
104
|
|
|
102
105
|
timestamp: SkipJsonSchema[int] = 0
|
|
103
106
|
tags: Annotated[list[str], PlainSerializer(",".join)] = [] # noqa: RUF012
|
|
104
|
-
state: SkipJsonSchema[Literal["published", "queued", "draft", "private", "unapproved"]] = "
|
|
107
|
+
state: SkipJsonSchema[Literal["published", "queued", "draft", "private", "unapproved"]] = "draft"
|
|
105
108
|
|
|
106
109
|
content: SkipJsonSchema[list[Block]] = [] # noqa: RUF012
|
|
107
110
|
layout: SkipJsonSchema[list[Block]] = [] # noqa: RUF012
|
|
@@ -1,21 +1,18 @@
|
|
|
1
1
|
from dataclasses import dataclass
|
|
2
2
|
from typing import Self
|
|
3
3
|
|
|
4
|
-
from niquests import HTTPError, Session
|
|
5
|
-
from requests import Response
|
|
6
|
-
from requests_cache import CacheMixin
|
|
4
|
+
from niquests import HTTPError, PreparedRequest, Response, Session
|
|
7
5
|
from requests_oauthlib import OAuth1
|
|
8
6
|
|
|
9
7
|
from tumblrbot.utils.models import Post, Tokens
|
|
10
8
|
|
|
11
9
|
|
|
12
10
|
@dataclass
|
|
13
|
-
class TumblrSession(
|
|
11
|
+
class TumblrSession(Session):
|
|
14
12
|
tokens: Tokens
|
|
15
13
|
|
|
16
14
|
def __post_init__(self) -> None:
|
|
17
|
-
|
|
18
|
-
Session.__init__(self, happy_eyeballs=True)
|
|
15
|
+
super().__init__(multiplexed=True, happy_eyeballs=True)
|
|
19
16
|
|
|
20
17
|
self.auth = OAuth1(**self.tokens.tumblr.model_dump(mode="json"))
|
|
21
18
|
self.hooks["response"].append(self.response_hook)
|
|
@@ -24,21 +21,22 @@ class TumblrSession(CacheMixin, Session): # pyright: ignore[reportIncompatibleM
|
|
|
24
21
|
super().__enter__()
|
|
25
22
|
return self
|
|
26
23
|
|
|
27
|
-
def response_hook(self, response:
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
24
|
+
def response_hook(self, response: PreparedRequest | Response) -> None:
|
|
25
|
+
if isinstance(response, Response):
|
|
26
|
+
try:
|
|
27
|
+
response.raise_for_status()
|
|
28
|
+
except HTTPError as error:
|
|
29
|
+
if response.text:
|
|
30
|
+
error.add_note(response.text)
|
|
31
|
+
raise
|
|
34
32
|
|
|
35
33
|
def retrieve_published_posts(self, blog_identifier: str, after: int) -> Response:
|
|
36
34
|
return self.get(
|
|
37
35
|
f"https://api.tumblr.com/v2/blog/{blog_identifier}/posts",
|
|
38
36
|
params={
|
|
39
|
-
"after": after,
|
|
37
|
+
"after": str(after),
|
|
40
38
|
"sort": "asc",
|
|
41
|
-
"npf": True,
|
|
39
|
+
"npf": str(True),
|
|
42
40
|
},
|
|
43
41
|
)
|
|
44
42
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|