markovka 0.1.0__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.
- markovka-0.1.0/LICENSE +24 -0
- markovka-0.1.0/PKG-INFO +154 -0
- markovka-0.1.0/README.md +145 -0
- markovka-0.1.0/markovka/__init__.py +0 -0
- markovka-0.1.0/markovka/chat.py +26 -0
- markovka-0.1.0/markovka/experiments.py +33 -0
- markovka-0.1.0/markovka/main.py +184 -0
- markovka-0.1.0/markovka/markov.py +125 -0
- markovka-0.1.0/markovka/stats.py +30 -0
- markovka-0.1.0/markovka/storage.py +30 -0
- markovka-0.1.0/markovka/utils.py +30 -0
- markovka-0.1.0/markovka.egg-info/PKG-INFO +154 -0
- markovka-0.1.0/markovka.egg-info/SOURCES.txt +16 -0
- markovka-0.1.0/markovka.egg-info/dependency_links.txt +1 -0
- markovka-0.1.0/markovka.egg-info/entry_points.txt +2 -0
- markovka-0.1.0/markovka.egg-info/top_level.txt +1 -0
- markovka-0.1.0/pyproject.toml +13 -0
- markovka-0.1.0/setup.cfg +4 -0
markovka-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
This is free and unencumbered software released into the public domain.
|
|
2
|
+
|
|
3
|
+
Anyone is free to copy, modify, publish, use, compile, sell, or
|
|
4
|
+
distribute this software, either in source code form or as a compiled
|
|
5
|
+
binary, for any purpose, commercial or non-commercial, and by any
|
|
6
|
+
means.
|
|
7
|
+
|
|
8
|
+
In jurisdictions that recognize copyright laws, the author or authors
|
|
9
|
+
of this software dedicate any and all copyright interest in the
|
|
10
|
+
software to the public domain. We make this dedication for the benefit
|
|
11
|
+
of the public at large and to the detriment of our heirs and
|
|
12
|
+
successors. We intend this dedication to be an overt act of
|
|
13
|
+
relinquishment in perpetuity of all present and future rights to this
|
|
14
|
+
software under copyright law.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
|
19
|
+
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
|
|
20
|
+
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
|
|
21
|
+
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
|
22
|
+
OTHER DEALINGS IN THE SOFTWARE.
|
|
23
|
+
|
|
24
|
+
For more information, please refer to <https://unlicense.org>
|
markovka-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: markovka
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Markov chain text generator with option of importing messenger chats and books as source material.
|
|
5
|
+
Requires-Python: >=3.10
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Dynamic: license-file
|
|
9
|
+
|
|
10
|
+
# Markov chain generator
|
|
11
|
+
|
|
12
|
+
[](https://forthebadge.com)
|
|
13
|
+
|
|
14
|
+
A command-line Markov chain text generator, that learnsf rom a book and writes new text in its style.
|
|
15
|
+
|
|
16
|
+
> The shy, unknown street? I shall be simply a puppy, and a pack of perspiration stood open. Peasants stood before his
|
|
17
|
+
|
|
18
|
+
1. it reads a book and builds tables of which words follow which 1, 2 or 3-wordsequences
|
|
19
|
+
2. generates text by picking the next word randomly, weighted by how often it appeared
|
|
20
|
+
3. uses longest memory when there's a real choice and uses backoff to fall back to shorter memory when there's only one option to avoid plagiarizing the source text
|
|
21
|
+
4. an originality checker that measures how much of the output is copied word for word
|
|
22
|
+
|
|
23
|
+
## Requirements
|
|
24
|
+
|
|
25
|
+
- Python 3 (no exterlan packages; only the standard library)
|
|
26
|
+
|
|
27
|
+
## Usage
|
|
28
|
+
|
|
29
|
+
Put a plain-text book from [Project Gutenberg](https://www.gutenberg.org) in the `books/` folder, then:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
python3 main.py <book_file> [options]
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
| Option | Default | Description |
|
|
36
|
+
| ---------------------- | ------- | ---------------------------------------------------------------------------------------------- |
|
|
37
|
+
| `--length` | 30 | How many words to generate |
|
|
38
|
+
| `--max-n` | 3 | Biggest memory size (n-gram order) |
|
|
39
|
+
| `--min-options` | 2 | Backoff threshold: minimum number of possible next words before falling back to shorter memory |
|
|
40
|
+
| `--count` | 1 | How many texts to generate |
|
|
41
|
+
| `--experiment` | off | Run the copy-score tests and the min_options experiment |
|
|
42
|
+
| `--override-sentences` | off | Don't force texts to start with a capital letter and end with . ! or ? |
|
|
43
|
+
| `--score` | off | Show how much of each text is copied word for word from the source material |
|
|
44
|
+
|
|
45
|
+
### Examples
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
python3 main.py crimeandpunishment.txt
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
wretched little beast was still life and strength that moment without this three roubles; I may have had nothing to do with you. I do no exactly.” “Do tell you something more essential
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
python3 main.py crimeandpunishment.txt --count 3 --length 50
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
spoke rather loud noise and animated conversation with a sharp little nose off with my sister--the day before yesterday.’ ‘And where did you go, will all keep him on the score of the old, dirty, by this. Then he remembered quite different. “Yes, Mr. Captain, and I am an ass; forgive myself for
|
|
61
|
+
why he went,” Raskolnikov by way will sing ‘My Village’ only, for the smart lady who was so unfortunate girl were asking your _fiancé_ in and weighed upon him. Here is some water. The feeling that it was like the latch on an empty tea-glass on a staircase, he mounted to tell you
|
|
62
|
+
scripture, too,” she was growing up, moved a night cap with new mourning had done _that_, could never bear that hardly audibly and God has smitten you, has no skill in such attire. Suddenly he withdrew his short-sighted eyes. You might well not such a fool I was afraid of!” Porfiry Petrovitch, receiving
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
````bash
|
|
66
|
+
python3 main.py crimeandpunishment.txt --experiment```
|
|
67
|
+
````
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
away in her will, ha-ha!). And she would not overlook anything. For the family who wanted a handkerchief, had turned the pocket is honourable. The vast quantities he said that he would not
|
|
71
|
+
202521
|
|
72
|
+
203612
|
|
73
|
+
--- SANITY TEST ---
|
|
74
|
+
1.0
|
|
75
|
+
--- ACTUAL TEST ---
|
|
76
|
+
0.034482758620689655
|
|
77
|
+
--- 1 ---
|
|
78
|
+
{3: 900}
|
|
79
|
+
0.9045977011494255
|
|
80
|
+
corner was disproportionately obtuse. There was scarcely any furniture in the big room: in the corner and several chairs--all government furniture, of polished yellow wood. In the further wall there was a time....
|
|
81
|
+
--- 2 ---
|
|
82
|
+
{1: 380, 2: 361, 3: 159}
|
|
83
|
+
0.08275862068965514
|
|
84
|
+
They’ve taken them food... I spoke about her mistrustfully, but that probably from his face. “And here... however queer, almost sullen insistence. “You’re right: she put them all so fond of Sonia? Sonia
|
|
85
|
+
--- 3 ---
|
|
86
|
+
{1: 509, 2: 313, 3: 78}
|
|
87
|
+
0.051724137931034475
|
|
88
|
+
this hesitating question, Sonia....” He stopped for me...” He rang the glass, put a stop here was the gate. She read loudly, cold with her own account, hasn’t said suddenly. “He is himself,”
|
|
89
|
+
--- 4 ---
|
|
90
|
+
{1: 555, 2: 288, 3: 57}
|
|
91
|
+
0.035632183908045977
|
|
92
|
+
and went in. He looked carefully and exactly what the doctor says!” Katerina Ivanovna, pale, thin spare the bridge. He became at parting for a poor student, or hunchback, I forgot! A plain,
|
|
93
|
+
--- 5 ---
|
|
94
|
+
{1: 599, 2: 266, 3: 35}
|
|
95
|
+
0.028735632183908046
|
|
96
|
+
“but what help the habit he said this with some other general’s.... Only just getting home in my opinions.... You know my opinion, and describing a change it, the business, and then to
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
python3 main.py crimeandpunishment.txt --score
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
After brief hesitation at the police office? Where’s the greatest suspicion of any folly.” They simply looked rather irregular for that moment when he stood with the axe in the noose, he tried to hang himself, and that I did not ask forgiveness, but he was in a fever of dust of the street.
|
|
105
|
+
copied: 18%
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## Results
|
|
109
|
+
|
|
110
|
+
Tested on _Crime and Punishment_ (about 200k words).
|
|
111
|
+
|
|
112
|
+
**Memory size vs copying** The longer the memory the more often there is one possible next word so the generator just copies the book:
|
|
113
|
+
|
|
114
|
+
| Memory | Keys with only one possible next word |
|
|
115
|
+
| ------- | ------------------------------------- |
|
|
116
|
+
| 1 word | 59% |
|
|
117
|
+
| 2 words | 82% |
|
|
118
|
+
| 3 words | 94% |
|
|
119
|
+
|
|
120
|
+
**Backoff vs copying** Copy score = share of 5-0word chunks in the output that appear word for word in the book (average of 30 texts per setting):
|
|
121
|
+
|
|
122
|
+
| min_options | 3-word memory used | Copy score |
|
|
123
|
+
| -------------- | ------------------ | ---------- |
|
|
124
|
+
| 1 (no backoff) | 100% | 91% |
|
|
125
|
+
| 2 | 16% | 7.5% |
|
|
126
|
+
| 3 | 10% | 6.1% |
|
|
127
|
+
| 4 | 5% | 2.6% |
|
|
128
|
+
| 5 | 6% | 2.6% |
|
|
129
|
+
|
|
130
|
+
backoff cuts the copying form 91% to about 7.5. above 2 copying barely drops and the text gets more mashed up. thats why 2 is the default
|
|
131
|
+
|
|
132
|
+
## Running tests
|
|
133
|
+
|
|
134
|
+
- Create and activate a .venv
|
|
135
|
+
- install pytest
|
|
136
|
+
- run pytest
|
|
137
|
+
Thet ests cover tables, copy score, header removal, cleaning and sentence boundiaries
|
|
138
|
+
|
|
139
|
+
## Known Limitations
|
|
140
|
+
|
|
141
|
+
- Sentence ends inside quotes like "Good-bye!" arent recognised as sentence ends
|
|
142
|
+
- a sentence start is detected by a capital letter so names in the middle of a sentence can also be piocked as starts
|
|
143
|
+
- in rare cases no sentence end is found within the limit and the program prints a warning
|
|
144
|
+
- for text without capitals or punctuation like chats use --override-sentences
|
|
145
|
+
|
|
146
|
+
## Roadmap
|
|
147
|
+
|
|
148
|
+
- Generating text from other data sources like JSON
|
|
149
|
+
- Cleaning the source text depending on the type
|
|
150
|
+
- Saving tables to a file
|
|
151
|
+
|
|
152
|
+
## Credits
|
|
153
|
+
|
|
154
|
+
- Book source: Project gutenberg, shoutout :)
|
markovka-0.1.0/README.md
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
# Markov chain generator
|
|
2
|
+
|
|
3
|
+
[](https://forthebadge.com)
|
|
4
|
+
|
|
5
|
+
A command-line Markov chain text generator, that learnsf rom a book and writes new text in its style.
|
|
6
|
+
|
|
7
|
+
> The shy, unknown street? I shall be simply a puppy, and a pack of perspiration stood open. Peasants stood before his
|
|
8
|
+
|
|
9
|
+
1. it reads a book and builds tables of which words follow which 1, 2 or 3-wordsequences
|
|
10
|
+
2. generates text by picking the next word randomly, weighted by how often it appeared
|
|
11
|
+
3. uses longest memory when there's a real choice and uses backoff to fall back to shorter memory when there's only one option to avoid plagiarizing the source text
|
|
12
|
+
4. an originality checker that measures how much of the output is copied word for word
|
|
13
|
+
|
|
14
|
+
## Requirements
|
|
15
|
+
|
|
16
|
+
- Python 3 (no exterlan packages; only the standard library)
|
|
17
|
+
|
|
18
|
+
## Usage
|
|
19
|
+
|
|
20
|
+
Put a plain-text book from [Project Gutenberg](https://www.gutenberg.org) in the `books/` folder, then:
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
python3 main.py <book_file> [options]
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
| Option | Default | Description |
|
|
27
|
+
| ---------------------- | ------- | ---------------------------------------------------------------------------------------------- |
|
|
28
|
+
| `--length` | 30 | How many words to generate |
|
|
29
|
+
| `--max-n` | 3 | Biggest memory size (n-gram order) |
|
|
30
|
+
| `--min-options` | 2 | Backoff threshold: minimum number of possible next words before falling back to shorter memory |
|
|
31
|
+
| `--count` | 1 | How many texts to generate |
|
|
32
|
+
| `--experiment` | off | Run the copy-score tests and the min_options experiment |
|
|
33
|
+
| `--override-sentences` | off | Don't force texts to start with a capital letter and end with . ! or ? |
|
|
34
|
+
| `--score` | off | Show how much of each text is copied word for word from the source material |
|
|
35
|
+
|
|
36
|
+
### Examples
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
python3 main.py crimeandpunishment.txt
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
wretched little beast was still life and strength that moment without this three roubles; I may have had nothing to do with you. I do no exactly.” “Do tell you something more essential
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
python3 main.py crimeandpunishment.txt --count 3 --length 50
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
spoke rather loud noise and animated conversation with a sharp little nose off with my sister--the day before yesterday.’ ‘And where did you go, will all keep him on the score of the old, dirty, by this. Then he remembered quite different. “Yes, Mr. Captain, and I am an ass; forgive myself for
|
|
52
|
+
why he went,” Raskolnikov by way will sing ‘My Village’ only, for the smart lady who was so unfortunate girl were asking your _fiancé_ in and weighed upon him. Here is some water. The feeling that it was like the latch on an empty tea-glass on a staircase, he mounted to tell you
|
|
53
|
+
scripture, too,” she was growing up, moved a night cap with new mourning had done _that_, could never bear that hardly audibly and God has smitten you, has no skill in such attire. Suddenly he withdrew his short-sighted eyes. You might well not such a fool I was afraid of!” Porfiry Petrovitch, receiving
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
````bash
|
|
57
|
+
python3 main.py crimeandpunishment.txt --experiment```
|
|
58
|
+
````
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
away in her will, ha-ha!). And she would not overlook anything. For the family who wanted a handkerchief, had turned the pocket is honourable. The vast quantities he said that he would not
|
|
62
|
+
202521
|
|
63
|
+
203612
|
|
64
|
+
--- SANITY TEST ---
|
|
65
|
+
1.0
|
|
66
|
+
--- ACTUAL TEST ---
|
|
67
|
+
0.034482758620689655
|
|
68
|
+
--- 1 ---
|
|
69
|
+
{3: 900}
|
|
70
|
+
0.9045977011494255
|
|
71
|
+
corner was disproportionately obtuse. There was scarcely any furniture in the big room: in the corner and several chairs--all government furniture, of polished yellow wood. In the further wall there was a time....
|
|
72
|
+
--- 2 ---
|
|
73
|
+
{1: 380, 2: 361, 3: 159}
|
|
74
|
+
0.08275862068965514
|
|
75
|
+
They’ve taken them food... I spoke about her mistrustfully, but that probably from his face. “And here... however queer, almost sullen insistence. “You’re right: she put them all so fond of Sonia? Sonia
|
|
76
|
+
--- 3 ---
|
|
77
|
+
{1: 509, 2: 313, 3: 78}
|
|
78
|
+
0.051724137931034475
|
|
79
|
+
this hesitating question, Sonia....” He stopped for me...” He rang the glass, put a stop here was the gate. She read loudly, cold with her own account, hasn’t said suddenly. “He is himself,”
|
|
80
|
+
--- 4 ---
|
|
81
|
+
{1: 555, 2: 288, 3: 57}
|
|
82
|
+
0.035632183908045977
|
|
83
|
+
and went in. He looked carefully and exactly what the doctor says!” Katerina Ivanovna, pale, thin spare the bridge. He became at parting for a poor student, or hunchback, I forgot! A plain,
|
|
84
|
+
--- 5 ---
|
|
85
|
+
{1: 599, 2: 266, 3: 35}
|
|
86
|
+
0.028735632183908046
|
|
87
|
+
“but what help the habit he said this with some other general’s.... Only just getting home in my opinions.... You know my opinion, and describing a change it, the business, and then to
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
python3 main.py crimeandpunishment.txt --score
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
After brief hesitation at the police office? Where’s the greatest suspicion of any folly.” They simply looked rather irregular for that moment when he stood with the axe in the noose, he tried to hang himself, and that I did not ask forgiveness, but he was in a fever of dust of the street.
|
|
96
|
+
copied: 18%
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## Results
|
|
100
|
+
|
|
101
|
+
Tested on _Crime and Punishment_ (about 200k words).
|
|
102
|
+
|
|
103
|
+
**Memory size vs copying** The longer the memory the more often there is one possible next word so the generator just copies the book:
|
|
104
|
+
|
|
105
|
+
| Memory | Keys with only one possible next word |
|
|
106
|
+
| ------- | ------------------------------------- |
|
|
107
|
+
| 1 word | 59% |
|
|
108
|
+
| 2 words | 82% |
|
|
109
|
+
| 3 words | 94% |
|
|
110
|
+
|
|
111
|
+
**Backoff vs copying** Copy score = share of 5-0word chunks in the output that appear word for word in the book (average of 30 texts per setting):
|
|
112
|
+
|
|
113
|
+
| min_options | 3-word memory used | Copy score |
|
|
114
|
+
| -------------- | ------------------ | ---------- |
|
|
115
|
+
| 1 (no backoff) | 100% | 91% |
|
|
116
|
+
| 2 | 16% | 7.5% |
|
|
117
|
+
| 3 | 10% | 6.1% |
|
|
118
|
+
| 4 | 5% | 2.6% |
|
|
119
|
+
| 5 | 6% | 2.6% |
|
|
120
|
+
|
|
121
|
+
backoff cuts the copying form 91% to about 7.5. above 2 copying barely drops and the text gets more mashed up. thats why 2 is the default
|
|
122
|
+
|
|
123
|
+
## Running tests
|
|
124
|
+
|
|
125
|
+
- Create and activate a .venv
|
|
126
|
+
- install pytest
|
|
127
|
+
- run pytest
|
|
128
|
+
Thet ests cover tables, copy score, header removal, cleaning and sentence boundiaries
|
|
129
|
+
|
|
130
|
+
## Known Limitations
|
|
131
|
+
|
|
132
|
+
- Sentence ends inside quotes like "Good-bye!" arent recognised as sentence ends
|
|
133
|
+
- a sentence start is detected by a capital letter so names in the middle of a sentence can also be piocked as starts
|
|
134
|
+
- in rare cases no sentence end is found within the limit and the program prints a warning
|
|
135
|
+
- for text without capitals or punctuation like chats use --override-sentences
|
|
136
|
+
|
|
137
|
+
## Roadmap
|
|
138
|
+
|
|
139
|
+
- Generating text from other data sources like JSON
|
|
140
|
+
- Cleaning the source text depending on the type
|
|
141
|
+
- Saving tables to a file
|
|
142
|
+
|
|
143
|
+
## Credits
|
|
144
|
+
|
|
145
|
+
- Book source: Project gutenberg, shoutout :)
|
|
File without changes
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import json
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def fix_encoding(text):
|
|
5
|
+
raw_bytes = text
|
|
6
|
+
fixed = raw_bytes.encode("latin1")
|
|
7
|
+
result = fixed.decode("utf-8")
|
|
8
|
+
return result
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def load_chat_messages(path, sender):
|
|
12
|
+
with open(path, encoding="utf-8") as f:
|
|
13
|
+
data = json.load(f)
|
|
14
|
+
messages = list()
|
|
15
|
+
for message in data["messages"]:
|
|
16
|
+
if "sender_name" in message:
|
|
17
|
+
name = fix_encoding(message["sender_name"])
|
|
18
|
+
text = message.get("content")
|
|
19
|
+
if text != None:
|
|
20
|
+
text = fix_encoding(text)
|
|
21
|
+
else:
|
|
22
|
+
name = message.get("senderName")
|
|
23
|
+
text = message.get("text")
|
|
24
|
+
if name == sender and text:
|
|
25
|
+
messages.append(text)
|
|
26
|
+
return messages
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
from markovka.markov import (
|
|
2
|
+
build_ngram_counts,
|
|
3
|
+
generate_text_backoff,
|
|
4
|
+
generate_text_ngram,
|
|
5
|
+
get_tables,
|
|
6
|
+
)
|
|
7
|
+
from markovka.stats import copy_score, share_single_options
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def print_result(words, max_n):
|
|
11
|
+
result = get_tables(words, max_n)
|
|
12
|
+
print(len(result[1]))
|
|
13
|
+
print(len(result[2]))
|
|
14
|
+
print(len(result[3]))
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def tune_min_options(tables, settings, runs, chunk_set):
|
|
18
|
+
for opt in settings:
|
|
19
|
+
totals = dict()
|
|
20
|
+
score_sum = 0
|
|
21
|
+
for _ in range(runs):
|
|
22
|
+
text, counts = generate_text_backoff(tables, 3, 30, min_options=opt)
|
|
23
|
+
score_sum += copy_score(text, chunk_set, 5)
|
|
24
|
+
for size in counts:
|
|
25
|
+
if size not in totals:
|
|
26
|
+
totals[size] = 0
|
|
27
|
+
totals[size] += counts[size]
|
|
28
|
+
average = score_sum / runs
|
|
29
|
+
|
|
30
|
+
print(f"--- {opt} ---")
|
|
31
|
+
print(totals)
|
|
32
|
+
print(average)
|
|
33
|
+
print(text)
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
|
|
3
|
+
from markovka.chat import load_chat_messages
|
|
4
|
+
from markovka.experiments import print_result, tune_min_options
|
|
5
|
+
from markovka.markov import (
|
|
6
|
+
generate_text_backoff,
|
|
7
|
+
get_tables,
|
|
8
|
+
get_tables_from_texts,
|
|
9
|
+
normalize,
|
|
10
|
+
)
|
|
11
|
+
from markovka.stats import book_chunks, copy_score
|
|
12
|
+
from markovka.storage import load_tables, save_tables
|
|
13
|
+
from markovka.utils import clean_text, get_book_text, remove_header
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def main():
|
|
17
|
+
parser = argparse.ArgumentParser()
|
|
18
|
+
parser.add_argument("book", nargs="*", help="file name of the book in books/")
|
|
19
|
+
parser.add_argument(
|
|
20
|
+
"--length", type=int, default=30, help="How many words to generate"
|
|
21
|
+
)
|
|
22
|
+
parser.add_argument(
|
|
23
|
+
"--max-n",
|
|
24
|
+
type=int,
|
|
25
|
+
default=3,
|
|
26
|
+
help="Biggest memory size (order of markov chain). 3 is default and best for books. try 2 for chats.",
|
|
27
|
+
)
|
|
28
|
+
parser.add_argument(
|
|
29
|
+
"--min-options",
|
|
30
|
+
type=int,
|
|
31
|
+
default=2,
|
|
32
|
+
help="best leave at default, copies the source text at 1 and is more gibberish at more than 2",
|
|
33
|
+
)
|
|
34
|
+
parser.add_argument(
|
|
35
|
+
"--count", type=int, default=1, help="Amount of texts to generate"
|
|
36
|
+
)
|
|
37
|
+
parser.add_argument(
|
|
38
|
+
"--experiment",
|
|
39
|
+
action="store_true",
|
|
40
|
+
help="run the copy-score tests and the min_options experiment",
|
|
41
|
+
)
|
|
42
|
+
parser.add_argument(
|
|
43
|
+
"--override-sentences",
|
|
44
|
+
action="store_true",
|
|
45
|
+
help="don't force texts to start with a capital and end with . ! or ?",
|
|
46
|
+
)
|
|
47
|
+
parser.add_argument(
|
|
48
|
+
"--score",
|
|
49
|
+
action="store_true",
|
|
50
|
+
help="Show how much of each text is copied from the source material",
|
|
51
|
+
)
|
|
52
|
+
parser.add_argument("--save", help="Save the tables to this JSON file")
|
|
53
|
+
parser.add_argument(
|
|
54
|
+
"--load", help="load tables from this JSON file instead of reading books"
|
|
55
|
+
)
|
|
56
|
+
parser.add_argument(
|
|
57
|
+
"--chat", nargs="+", help="one or more chat export files in chats/"
|
|
58
|
+
)
|
|
59
|
+
parser.add_argument(
|
|
60
|
+
"--sender", help="whose messages to learn from (exact name as in the export)"
|
|
61
|
+
)
|
|
62
|
+
parser.add_argument(
|
|
63
|
+
"--score-k",
|
|
64
|
+
type=int,
|
|
65
|
+
default=5,
|
|
66
|
+
help="How many words in a row count as copied, try 3 for chats",
|
|
67
|
+
)
|
|
68
|
+
parser.add_argument("--talk", action="store_true", help="talk with the generator!")
|
|
69
|
+
args = parser.parse_args()
|
|
70
|
+
if not args.book and args.load is None and args.chat is None:
|
|
71
|
+
parser.error("Give at least one book, --chat FILES or --load FILE")
|
|
72
|
+
if args.chat and args.sender is None:
|
|
73
|
+
parser.error("--chat needs --sender NAME")
|
|
74
|
+
if args.sender and args.chat is None:
|
|
75
|
+
parser.error("Did you mean --chat?")
|
|
76
|
+
|
|
77
|
+
if args.load and (args.book or args.chat):
|
|
78
|
+
parser.error("Use either books or --load, not both")
|
|
79
|
+
if args.load and (args.score or args.experiment):
|
|
80
|
+
parser.error("--score and --experiment need the book text not just tables")
|
|
81
|
+
if args.load:
|
|
82
|
+
try:
|
|
83
|
+
tables = load_tables(args.load)
|
|
84
|
+
except FileNotFoundError:
|
|
85
|
+
parser.error(f"file {args.load} not found")
|
|
86
|
+
else:
|
|
87
|
+
book_words = list()
|
|
88
|
+
texts = list()
|
|
89
|
+
for book_name in args.book:
|
|
90
|
+
book_path = f"books/{book_name}"
|
|
91
|
+
try:
|
|
92
|
+
book_text = get_book_text(book_path)
|
|
93
|
+
except FileNotFoundError:
|
|
94
|
+
parser.error(f"Book {book_name} not found")
|
|
95
|
+
no_header = remove_header(book_text)
|
|
96
|
+
cleaned = clean_text(no_header)
|
|
97
|
+
book_words.extend(cleaned.split())
|
|
98
|
+
texts.append(cleaned.split())
|
|
99
|
+
if args.chat:
|
|
100
|
+
for chat_name in args.chat:
|
|
101
|
+
chat_path = f"chats/{chat_name}"
|
|
102
|
+
chat_messages = load_chat_messages(chat_path, args.sender)
|
|
103
|
+
for message in chat_messages:
|
|
104
|
+
book_words.extend(message.split())
|
|
105
|
+
texts.append(message.split())
|
|
106
|
+
tables = get_tables_from_texts(texts, args.max_n)
|
|
107
|
+
max_n = len(tables)
|
|
108
|
+
if args.save:
|
|
109
|
+
save_tables(tables, args.save)
|
|
110
|
+
|
|
111
|
+
if args.score or args.experiment:
|
|
112
|
+
book_chunk_set = book_chunks(book_words, args.score_k)
|
|
113
|
+
score_sum = 0
|
|
114
|
+
if args.talk:
|
|
115
|
+
known = set()
|
|
116
|
+
for key in tables[max_n]:
|
|
117
|
+
known.add(normalize(key[0]))
|
|
118
|
+
while True:
|
|
119
|
+
try:
|
|
120
|
+
user = input("you: ")
|
|
121
|
+
except (KeyboardInterrupt, EOFError):
|
|
122
|
+
print()
|
|
123
|
+
break
|
|
124
|
+
if user == "/quit":
|
|
125
|
+
break
|
|
126
|
+
candidates = list()
|
|
127
|
+
for word in user.split():
|
|
128
|
+
if normalize(word) in known:
|
|
129
|
+
candidates.append(word)
|
|
130
|
+
if not candidates:
|
|
131
|
+
start = None
|
|
132
|
+
else:
|
|
133
|
+
start = max(candidates, key=len)
|
|
134
|
+
text, counts = generate_text_backoff(
|
|
135
|
+
tables,
|
|
136
|
+
max_n,
|
|
137
|
+
args.length,
|
|
138
|
+
min_options=args.min_options,
|
|
139
|
+
sentences=not args.override_sentences,
|
|
140
|
+
start_word=start,
|
|
141
|
+
)
|
|
142
|
+
print(f"bot: {text}")
|
|
143
|
+
else:
|
|
144
|
+
for _ in range(args.count):
|
|
145
|
+
text, counts = generate_text_backoff(
|
|
146
|
+
tables,
|
|
147
|
+
max_n,
|
|
148
|
+
args.length,
|
|
149
|
+
min_options=args.min_options,
|
|
150
|
+
sentences=not args.override_sentences,
|
|
151
|
+
)
|
|
152
|
+
print(text)
|
|
153
|
+
if (
|
|
154
|
+
not args.override_sentences
|
|
155
|
+
and text[-1].endswith((".", "!", "?")) == False
|
|
156
|
+
):
|
|
157
|
+
print(
|
|
158
|
+
"Error! Couldn't generate text ending with a sentence ending mark. Try again, or run with --override-sentences"
|
|
159
|
+
)
|
|
160
|
+
print("-------------")
|
|
161
|
+
if args.score:
|
|
162
|
+
score = copy_score(text, book_chunk_set, args.score_k)
|
|
163
|
+
score_sum += score
|
|
164
|
+
percent = round(score * 100)
|
|
165
|
+
print(f"copied: {percent}%")
|
|
166
|
+
if args.score:
|
|
167
|
+
print("average copied")
|
|
168
|
+
print(round(score_sum / args.count * 100))
|
|
169
|
+
|
|
170
|
+
if args.experiment:
|
|
171
|
+
print(len(book_chunk_set))
|
|
172
|
+
print(len(book_words))
|
|
173
|
+
test_text = """On an exceptionally hot evening early in July a young man came out of
|
|
174
|
+
the garret in which he lodged in S. Place and walked slowly, as though
|
|
175
|
+
in hesitation,"""
|
|
176
|
+
print("--- SANITY TEST ---")
|
|
177
|
+
print(copy_score(test_text, book_chunk_set, 5))
|
|
178
|
+
print("--- ACTUAL TEST --- ")
|
|
179
|
+
print(copy_score(text, book_chunk_set, 5))
|
|
180
|
+
tune_min_options(tables, range(1, 6), 30, book_chunk_set)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
if __name__ == "__main__":
|
|
184
|
+
main()
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import random
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
# build the table
|
|
5
|
+
def build_ngram_counts(words, n, counts=None):
|
|
6
|
+
if counts == None:
|
|
7
|
+
ngram_counts = {}
|
|
8
|
+
else:
|
|
9
|
+
ngram_counts = counts
|
|
10
|
+
for i in range(0, len(words) - n):
|
|
11
|
+
current_key = tuple(words[i : i + n])
|
|
12
|
+
next_word = words[i + n]
|
|
13
|
+
if current_key not in ngram_counts:
|
|
14
|
+
ngram_counts[current_key] = dict()
|
|
15
|
+
if next_word not in ngram_counts[current_key]:
|
|
16
|
+
ngram_counts[current_key][next_word] = 0
|
|
17
|
+
|
|
18
|
+
ngram_counts[current_key][next_word] += 1
|
|
19
|
+
return ngram_counts
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
# generating text
|
|
23
|
+
def generate_text_ngram(table, length):
|
|
24
|
+
all_keys = list(table.keys())
|
|
25
|
+
current = random.choice(all_keys)
|
|
26
|
+
generated = list(current)
|
|
27
|
+
n = len(current)
|
|
28
|
+
|
|
29
|
+
for i in range(length):
|
|
30
|
+
if current not in table:
|
|
31
|
+
current = random.choice(all_keys)
|
|
32
|
+
options = list(table[current].keys())
|
|
33
|
+
weights = list(table[current].values())
|
|
34
|
+
next_word = random.choices(options, weights=weights)[0]
|
|
35
|
+
generated.append(next_word)
|
|
36
|
+
current = tuple(generated[-n:])
|
|
37
|
+
result_seperator = " "
|
|
38
|
+
result = result_seperator.join(generated)
|
|
39
|
+
return result
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def generate_text_backoff(
|
|
43
|
+
tables, max_n, length, min_options=2, sentences=True, start_word=None
|
|
44
|
+
):
|
|
45
|
+
all_keys = list(tables[max_n].keys())
|
|
46
|
+
start_keys = list()
|
|
47
|
+
for key in all_keys:
|
|
48
|
+
if key[0][0].isupper():
|
|
49
|
+
start_keys.append(key)
|
|
50
|
+
if len(start_keys) == 0:
|
|
51
|
+
start_keys = all_keys
|
|
52
|
+
if sentences == False:
|
|
53
|
+
start_keys = all_keys
|
|
54
|
+
if start_word != None:
|
|
55
|
+
matching = list()
|
|
56
|
+
for key in all_keys:
|
|
57
|
+
if key[0] == start_word:
|
|
58
|
+
matching.append(key)
|
|
59
|
+
if len(matching) > 0:
|
|
60
|
+
start_keys = matching
|
|
61
|
+
current = random.choice(start_keys)
|
|
62
|
+
generated = list(current)
|
|
63
|
+
sizes_used = dict()
|
|
64
|
+
if sentences == True:
|
|
65
|
+
max_rounds = length * 3
|
|
66
|
+
else:
|
|
67
|
+
max_rounds = length
|
|
68
|
+
for i in range(max_rounds):
|
|
69
|
+
picked = None
|
|
70
|
+
for n in range(max_n, 0, -1):
|
|
71
|
+
key = tuple(generated[-n:])
|
|
72
|
+
table = tables[n]
|
|
73
|
+
if key in table and (len(table[key]) >= min_options or n == 1):
|
|
74
|
+
options = list(table[key].keys())
|
|
75
|
+
weights = list(table[key].values())
|
|
76
|
+
picked = random.choices(options, weights=weights)[0]
|
|
77
|
+
if n not in sizes_used:
|
|
78
|
+
sizes_used[n] = 0
|
|
79
|
+
sizes_used[n] += 1
|
|
80
|
+
break
|
|
81
|
+
if picked is None:
|
|
82
|
+
single_word_table = tables[1]
|
|
83
|
+
keys = single_word_table.keys()
|
|
84
|
+
key_list = list(keys)
|
|
85
|
+
random_key = random.choice(key_list)
|
|
86
|
+
picked = random_key[0]
|
|
87
|
+
if "jump" not in sizes_used:
|
|
88
|
+
sizes_used["jump"] = 0
|
|
89
|
+
sizes_used["jump"] += 1
|
|
90
|
+
generated.append(picked)
|
|
91
|
+
if sentences and i + 1 >= length and generated[-1].endswith((".", "!", "?")):
|
|
92
|
+
break
|
|
93
|
+
result_seperator = " "
|
|
94
|
+
result = result_seperator.join(generated)
|
|
95
|
+
return result, sizes_used
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def get_tables(words, max_n):
|
|
99
|
+
tables = dict()
|
|
100
|
+
for n in range(1, max_n + 1):
|
|
101
|
+
tables[n] = build_ngram_counts(words, n)
|
|
102
|
+
return tables
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def get_tables_from_texts(texts, max_n):
|
|
106
|
+
tables = dict()
|
|
107
|
+
for n in range(1, max_n + 1):
|
|
108
|
+
tables[n] = dict()
|
|
109
|
+
for words in texts:
|
|
110
|
+
for n in range(1, max_n + 1):
|
|
111
|
+
build_ngram_counts(words, n, tables[n])
|
|
112
|
+
return tables
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def print_result(words, max_n):
|
|
116
|
+
result = get_tables(words, max_n)
|
|
117
|
+
print(len(result[1]))
|
|
118
|
+
print(len(result[2]))
|
|
119
|
+
print(len(result[3]))
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def normalize(word):
|
|
123
|
+
lowercase = word.lower()
|
|
124
|
+
stripped = lowercase.strip(".,!?")
|
|
125
|
+
return stripped
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
def share_single_options(table):
|
|
2
|
+
single_count = 0
|
|
3
|
+
for followers in table.values():
|
|
4
|
+
if len(followers) == 1:
|
|
5
|
+
single_count += 1
|
|
6
|
+
return single_count / len(table)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def book_chunks(words, k):
|
|
10
|
+
chunks = set()
|
|
11
|
+
last_start = len(words) - k
|
|
12
|
+
for i in range(last_start + 1):
|
|
13
|
+
chunk = tuple(words[i : i + k])
|
|
14
|
+
chunks.add(chunk)
|
|
15
|
+
return chunks
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def copy_score(text, chunk_set, k):
|
|
19
|
+
text_words = text.split()
|
|
20
|
+
all_chunks = 0
|
|
21
|
+
found_chunk = 0
|
|
22
|
+
last_start = len(text_words) - k
|
|
23
|
+
for i in range(last_start + 1):
|
|
24
|
+
chunk = tuple(text_words[i : i + k])
|
|
25
|
+
all_chunks += 1
|
|
26
|
+
if chunk in chunk_set:
|
|
27
|
+
found_chunk += 1
|
|
28
|
+
if all_chunks == 0:
|
|
29
|
+
return 0.0
|
|
30
|
+
return found_chunk / all_chunks
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import json
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def save_tables(tables, path):
|
|
5
|
+
json_ready = dict()
|
|
6
|
+
key_seperator = " "
|
|
7
|
+
for n in tables:
|
|
8
|
+
table = tables[n]
|
|
9
|
+
string_table = dict()
|
|
10
|
+
for key in table:
|
|
11
|
+
string_key = key_seperator.join(key)
|
|
12
|
+
string_table[string_key] = table[key]
|
|
13
|
+
json_ready[n] = string_table
|
|
14
|
+
with open(path, "w", encoding="utf-8") as f:
|
|
15
|
+
json.dump(json_ready, f, ensure_ascii=False)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def load_tables(path):
|
|
19
|
+
with open(path, encoding="utf-8") as f:
|
|
20
|
+
loaded = json.load(f)
|
|
21
|
+
tables = dict()
|
|
22
|
+
for n_string in loaded:
|
|
23
|
+
n = int(n_string)
|
|
24
|
+
string_table = loaded[n_string]
|
|
25
|
+
table = dict()
|
|
26
|
+
for string_key in string_table:
|
|
27
|
+
key = tuple(string_key.split())
|
|
28
|
+
table[key] = string_table[string_key]
|
|
29
|
+
tables[n] = table
|
|
30
|
+
return tables
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import random
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def remove_header(text):
|
|
5
|
+
try:
|
|
6
|
+
start_book = text.index("START OF THE PROJECT GUTENBERG EBOOK")
|
|
7
|
+
marker_end = text.index("***", start_book)
|
|
8
|
+
end_book = text.index("*** END OF")
|
|
9
|
+
no_header = text[marker_end + 3 : end_book]
|
|
10
|
+
return no_header
|
|
11
|
+
except ValueError:
|
|
12
|
+
return text
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def get_book_text(path):
|
|
16
|
+
with open(path) as f:
|
|
17
|
+
return f.read()
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def clean_text(text):
|
|
21
|
+
lines = text.splitlines()
|
|
22
|
+
kept = list()
|
|
23
|
+
for line in lines:
|
|
24
|
+
stripped = line.strip()
|
|
25
|
+
if stripped.startswith("CHAPTER") or stripped.startswith("PART"):
|
|
26
|
+
continue
|
|
27
|
+
kept.append(stripped)
|
|
28
|
+
result = "\n".join(kept)
|
|
29
|
+
no_april_underscores = result.replace("_", "")
|
|
30
|
+
return no_april_underscores
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: markovka
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Markov chain text generator with option of importing messenger chats and books as source material.
|
|
5
|
+
Requires-Python: >=3.10
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Dynamic: license-file
|
|
9
|
+
|
|
10
|
+
# Markov chain generator
|
|
11
|
+
|
|
12
|
+
[](https://forthebadge.com)
|
|
13
|
+
|
|
14
|
+
A command-line Markov chain text generator, that learnsf rom a book and writes new text in its style.
|
|
15
|
+
|
|
16
|
+
> The shy, unknown street? I shall be simply a puppy, and a pack of perspiration stood open. Peasants stood before his
|
|
17
|
+
|
|
18
|
+
1. it reads a book and builds tables of which words follow which 1, 2 or 3-wordsequences
|
|
19
|
+
2. generates text by picking the next word randomly, weighted by how often it appeared
|
|
20
|
+
3. uses longest memory when there's a real choice and uses backoff to fall back to shorter memory when there's only one option to avoid plagiarizing the source text
|
|
21
|
+
4. an originality checker that measures how much of the output is copied word for word
|
|
22
|
+
|
|
23
|
+
## Requirements
|
|
24
|
+
|
|
25
|
+
- Python 3 (no exterlan packages; only the standard library)
|
|
26
|
+
|
|
27
|
+
## Usage
|
|
28
|
+
|
|
29
|
+
Put a plain-text book from [Project Gutenberg](https://www.gutenberg.org) in the `books/` folder, then:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
python3 main.py <book_file> [options]
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
| Option | Default | Description |
|
|
36
|
+
| ---------------------- | ------- | ---------------------------------------------------------------------------------------------- |
|
|
37
|
+
| `--length` | 30 | How many words to generate |
|
|
38
|
+
| `--max-n` | 3 | Biggest memory size (n-gram order) |
|
|
39
|
+
| `--min-options` | 2 | Backoff threshold: minimum number of possible next words before falling back to shorter memory |
|
|
40
|
+
| `--count` | 1 | How many texts to generate |
|
|
41
|
+
| `--experiment` | off | Run the copy-score tests and the min_options experiment |
|
|
42
|
+
| `--override-sentences` | off | Don't force texts to start with a capital letter and end with . ! or ? |
|
|
43
|
+
| `--score` | off | Show how much of each text is copied word for word from the source material |
|
|
44
|
+
|
|
45
|
+
### Examples
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
python3 main.py crimeandpunishment.txt
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
wretched little beast was still life and strength that moment without this three roubles; I may have had nothing to do with you. I do no exactly.” “Do tell you something more essential
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
python3 main.py crimeandpunishment.txt --count 3 --length 50
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
spoke rather loud noise and animated conversation with a sharp little nose off with my sister--the day before yesterday.’ ‘And where did you go, will all keep him on the score of the old, dirty, by this. Then he remembered quite different. “Yes, Mr. Captain, and I am an ass; forgive myself for
|
|
61
|
+
why he went,” Raskolnikov by way will sing ‘My Village’ only, for the smart lady who was so unfortunate girl were asking your _fiancé_ in and weighed upon him. Here is some water. The feeling that it was like the latch on an empty tea-glass on a staircase, he mounted to tell you
|
|
62
|
+
scripture, too,” she was growing up, moved a night cap with new mourning had done _that_, could never bear that hardly audibly and God has smitten you, has no skill in such attire. Suddenly he withdrew his short-sighted eyes. You might well not such a fool I was afraid of!” Porfiry Petrovitch, receiving
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
````bash
|
|
66
|
+
python3 main.py crimeandpunishment.txt --experiment```
|
|
67
|
+
````
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
away in her will, ha-ha!). And she would not overlook anything. For the family who wanted a handkerchief, had turned the pocket is honourable. The vast quantities he said that he would not
|
|
71
|
+
202521
|
|
72
|
+
203612
|
|
73
|
+
--- SANITY TEST ---
|
|
74
|
+
1.0
|
|
75
|
+
--- ACTUAL TEST ---
|
|
76
|
+
0.034482758620689655
|
|
77
|
+
--- 1 ---
|
|
78
|
+
{3: 900}
|
|
79
|
+
0.9045977011494255
|
|
80
|
+
corner was disproportionately obtuse. There was scarcely any furniture in the big room: in the corner and several chairs--all government furniture, of polished yellow wood. In the further wall there was a time....
|
|
81
|
+
--- 2 ---
|
|
82
|
+
{1: 380, 2: 361, 3: 159}
|
|
83
|
+
0.08275862068965514
|
|
84
|
+
They’ve taken them food... I spoke about her mistrustfully, but that probably from his face. “And here... however queer, almost sullen insistence. “You’re right: she put them all so fond of Sonia? Sonia
|
|
85
|
+
--- 3 ---
|
|
86
|
+
{1: 509, 2: 313, 3: 78}
|
|
87
|
+
0.051724137931034475
|
|
88
|
+
this hesitating question, Sonia....” He stopped for me...” He rang the glass, put a stop here was the gate. She read loudly, cold with her own account, hasn’t said suddenly. “He is himself,”
|
|
89
|
+
--- 4 ---
|
|
90
|
+
{1: 555, 2: 288, 3: 57}
|
|
91
|
+
0.035632183908045977
|
|
92
|
+
and went in. He looked carefully and exactly what the doctor says!” Katerina Ivanovna, pale, thin spare the bridge. He became at parting for a poor student, or hunchback, I forgot! A plain,
|
|
93
|
+
--- 5 ---
|
|
94
|
+
{1: 599, 2: 266, 3: 35}
|
|
95
|
+
0.028735632183908046
|
|
96
|
+
“but what help the habit he said this with some other general’s.... Only just getting home in my opinions.... You know my opinion, and describing a change it, the business, and then to
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
python3 main.py crimeandpunishment.txt --score
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
After brief hesitation at the police office? Where’s the greatest suspicion of any folly.” They simply looked rather irregular for that moment when he stood with the axe in the noose, he tried to hang himself, and that I did not ask forgiveness, but he was in a fever of dust of the street.
|
|
105
|
+
copied: 18%
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## Results
|
|
109
|
+
|
|
110
|
+
Tested on _Crime and Punishment_ (about 200k words).
|
|
111
|
+
|
|
112
|
+
**Memory size vs copying** The longer the memory the more often there is one possible next word so the generator just copies the book:
|
|
113
|
+
|
|
114
|
+
| Memory | Keys with only one possible next word |
|
|
115
|
+
| ------- | ------------------------------------- |
|
|
116
|
+
| 1 word | 59% |
|
|
117
|
+
| 2 words | 82% |
|
|
118
|
+
| 3 words | 94% |
|
|
119
|
+
|
|
120
|
+
**Backoff vs copying** Copy score = share of 5-0word chunks in the output that appear word for word in the book (average of 30 texts per setting):
|
|
121
|
+
|
|
122
|
+
| min_options | 3-word memory used | Copy score |
|
|
123
|
+
| -------------- | ------------------ | ---------- |
|
|
124
|
+
| 1 (no backoff) | 100% | 91% |
|
|
125
|
+
| 2 | 16% | 7.5% |
|
|
126
|
+
| 3 | 10% | 6.1% |
|
|
127
|
+
| 4 | 5% | 2.6% |
|
|
128
|
+
| 5 | 6% | 2.6% |
|
|
129
|
+
|
|
130
|
+
backoff cuts the copying form 91% to about 7.5. above 2 copying barely drops and the text gets more mashed up. thats why 2 is the default
|
|
131
|
+
|
|
132
|
+
## Running tests
|
|
133
|
+
|
|
134
|
+
- Create and activate a .venv
|
|
135
|
+
- install pytest
|
|
136
|
+
- run pytest
|
|
137
|
+
Thet ests cover tables, copy score, header removal, cleaning and sentence boundiaries
|
|
138
|
+
|
|
139
|
+
## Known Limitations
|
|
140
|
+
|
|
141
|
+
- Sentence ends inside quotes like "Good-bye!" arent recognised as sentence ends
|
|
142
|
+
- a sentence start is detected by a capital letter so names in the middle of a sentence can also be piocked as starts
|
|
143
|
+
- in rare cases no sentence end is found within the limit and the program prints a warning
|
|
144
|
+
- for text without capitals or punctuation like chats use --override-sentences
|
|
145
|
+
|
|
146
|
+
## Roadmap
|
|
147
|
+
|
|
148
|
+
- Generating text from other data sources like JSON
|
|
149
|
+
- Cleaning the source text depending on the type
|
|
150
|
+
- Saving tables to a file
|
|
151
|
+
|
|
152
|
+
## Credits
|
|
153
|
+
|
|
154
|
+
- Book source: Project gutenberg, shoutout :)
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
markovka/__init__.py
|
|
5
|
+
markovka/chat.py
|
|
6
|
+
markovka/experiments.py
|
|
7
|
+
markovka/main.py
|
|
8
|
+
markovka/markov.py
|
|
9
|
+
markovka/stats.py
|
|
10
|
+
markovka/storage.py
|
|
11
|
+
markovka/utils.py
|
|
12
|
+
markovka.egg-info/PKG-INFO
|
|
13
|
+
markovka.egg-info/SOURCES.txt
|
|
14
|
+
markovka.egg-info/dependency_links.txt
|
|
15
|
+
markovka.egg-info/entry_points.txt
|
|
16
|
+
markovka.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
markovka
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools >= 77.0.3"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
[project]
|
|
5
|
+
name = "markovka"
|
|
6
|
+
version = "0.1.0"
|
|
7
|
+
description = "Markov chain text generator with option of importing messenger chats and books as source material."
|
|
8
|
+
readme = "README.md"
|
|
9
|
+
requires-python = ">=3.10"
|
|
10
|
+
[project.scripts]
|
|
11
|
+
markovka = "markovka.main:main"
|
|
12
|
+
[tool.setuptools]
|
|
13
|
+
packages = ["markovka"]
|
markovka-0.1.0/setup.cfg
ADDED