Initial revision.
[robmyers:glitcherature.git] / glitcherature.py
1 # -*- coding: utf-8 -*-
2
3 # glitcherature.py - Functions to glitch and generate text.
4 # Copyright (C) 2012  Rob Myers <rob@robmyers.org>
5 #
6 # This program is free software: you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation, either version 3 of the License, or 
9 # (at your option) any later version.
10 #
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14 # GNU General Public License for more details.
15 #
16 # You should have received a copy of the GNU General Public License
17 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
18
19 import random
20 import re
21 import string
22 from nltk.corpus import wordnet
23
24 ################################################################################
25 # Utilities
26 ################################################################################
27
28 def one_in(n):
29     """ Return True 1/n times, False the rest of the time.
30         n can be int or floate"""
31     return random.random() * n <= 1
32
33 def maybe(fun, default, n):
34     """ Return the results of calling fun() 1/n times, otherwise default"""
35     result = default
36     if one_in(n):
37         result = fun()
38     return result
39
40 ################################################################################
41 # Generating text
42 ################################################################################
43
44 def repeat_run(item, length):
45     """Return a string repeating item for the given length"""
46     return ''.join(item * length)
47
48 def random_run(items, length):
49     """Return a string of length randomly chosen items"""
50     return ''.join([random.choice(items) for i in xrange(length)])
51
52 ################################################################################
53 # General: apply to words, sentences, paragraphs
54 ################################################################################
55
56 def wrap(item, before, after):
57     """Wrap item in before and after"""
58     return "%s%s%s" % (before, item, after)
59
60 ################################################################################
61 # Words
62 # Operate on (and generate) strings of characters without whitespace
63 ################################################################################
64
65 def split_word(word, probability=1):
66     # Find subword using nltk or something, e.g.: antidote -> anti, dote
67     pass
68
69 def synset(word):
70     """Try to get a Wordnet synset for word, trying noun then verb"""
71     synset = None
72     try:
73         synset = wordnet.synset("%s.n.01" % word)
74     except:
75         try:
76             result = wordnet.synset("%s.v.01" % word)
77         except:
78             pass
79     return synset
80
81 def synset_name(synset):
82     """Get the English word for the synset"""
83     name = re.match(r"[^.]+", synset.name).group(0)
84     name = re.sub("_", " ", name)
85     return name
86
87 def hypernym(word):
88     """Choose a random hypernym for the word"""
89     result = word
90     synset = synset(word)
91     try:
92         synonym = random.choice(synset.hypernyms())
93         result = synset_name(synonym)
94     except:
95         pass
96     return result
97
98 def hyponym(word):
99     """Choose a random hyponym for the word"""
100     result = word
101     synset = wordnet_synset(word)
102     try:
103         hyponym = random.choice(synset.hyponyms)
104         result = synset_name(hyponym)
105     except:
106         pass
107     return result
108
109 def example(word):
110     """Get a piece of text that is an example of using the word, or None"""
111     result = None
112     synset = wordnet_synset(word)
113     if synset:
114         result = random.choice(synset.examples)
115     return result
116
117 ################################################################################
118 # Sentences
119 # Operate on strings of whitespace-separated words with punctuation.
120 ################################################################################
121
122 def replace_spaces(text, replacement, n=1):
123     """Replace spaces with replacement 1/n times"""
124     return re.sub(r" ",
125                   lambda match: replacement if one_in(n) else match.group(0),
126                   text)
127
128 def all_lower(text):
129     """Make everything lower-case"""
130     return text.lower()
131
132 def all_upper(text):
133     """Make everything upper-case"""
134     return text.upper()
135
136 def random_upper_letters(text, n=2):
137     """Randomly set letters to upper-case"""
138     return ''.join([letter.upper() if one_in(n) else letter for letter in text])
139
140 def random_upper_words(text, n=2):
141     """Randomly set words to all upper-case"""
142     return re.sub(r"\w+",
143                   lambda word: word.group(0).upper() if one_in(n) \
144                       else word.group(0),
145                   text)
146
147 def sub_text(text, subs, n=1):
148     """Substitute keys from subs for values from subs in text 1/n of the time"""
149     return re.sub(r"\w+",
150                   lambda word: subs.get(word, word) if one_in(n) else word,
151                   text)
152
153 leet = {'a': ('@', '/\\', '/-\\', 'λ'),
154         'b': ('8', '|3', '6', ']3'),
155         'c': ('(', '{', '<', '©'),
156         'd': ('|)', '|]', '])', '∂'),
157         'e': ('3', '£', '€', '='),
158         'f': ('ʃ', '|=', ']=', ')='),
159         'g': ('6', '9', '&', 'C-'),
160         'h': ('|-|', '#', '}{', ')-('),
161         'i': ('!', '1', '|', '`|'),
162         'j': ('_|', ']', '_/', '_)'),
163         'k': ('|<', '|{', '|X', ']<'),
164         'l': ('1', '7', '|_', '|'),
165         'm': ('44', '/\\/\\', '|\\/|', '|v|'),
166         'n': ('|\\|', '/\\/', 'И', '~'),
167         'o': ('()', '[]', '0', 'Ø'),
168         'p': ('|*', '?', '9', '|"'),
169         'q': ('0_', '0,', '(),', '¶'),
170         'r': ('®', 'Я', 'I^', '|2'),
171         's': ('$', '5', '§', '_\-'),
172         't': ('7', '+', '†', '|['),
173         'u': ('\\/', '|_|', 'μ', '/_/'),
174         'v': ('\\\\//', '\\/', '√', 'V'),
175         'w': ('vv', '\\/\\/', 'Ш', '\\^/'),
176         'x': ('%', '><', '*', 'Ж'),
177         'y': ('`/', "'/", '`(', '-/'),
178         'z': ('2', '3', '`/_', '%')}
179
180 leet_kinds_count = len(leet['a'])
181
182 # Make sure we have the same number of options for every letter
183
184 assert(all([len(leets) == leet_kinds_count for leets in leet.itervalues()]))
185
186 # Make sure no mappings appear twice in the same column
187
188 assert(all([len(set(row[i] for row in leet.itervalues())) == len(leet)
189             for i in xrange(0, len(leet['a']))]))
190
191 def sub_1337_vowels(text, n=1):
192     """Replace vowels in the text with 1337 1/n of the time"""
193     trans = string.maketrans("aeioul", "@3|0\"1") 
194     return ''.join([c.translate(trans) if one_in(n) else c for c in text])
195
196 def sub_1337_char(char, kind, n=1):
197     """Substitute the char for 1337 kind 1/n of the time"""
198     result = char
199     if char in leet and one_in(n):
200         result = leet[char][kind]
201     return result
202
203 def sub_1337(text, kind, n=1):
204     """Replace letters in the text with the given kind (1..4) of 1337 1/n"""
205     return ''.join([sub_1337_char(c, kind, n) for c in text])
206
207 txt = {'are': 'r',
208        'be': 'b',
209        'hate': 'h8',
210        'late': 'l8',
211        'see': 'c',
212        'you': 'u',}
213
214 def sub_txt(text, n=1):
215     """Substitute words in text for their txt equivalents 1/n of the time"""
216     for key in txt.iterkeys():
217         text = re.sub("\\b%s" % key,
218                   lambda x: txt[key] if one_in(n) else key,
219                   text,
220                   flags=re.IGNORECASE)
221     return text
222
223 def sub_space_runs(text, sub, min_len, max_len):
224     """Substitute spaces with runs of sub character, or choices from sub list"""
225     if type(sub) != list:
226         sub = [sub]
227     return re.sub(r"\s+",
228                   lambda spaces: repeat_run(random.choice(sub),
229                                             random.randint(min_len, max_len)),
230                   text)
231
232 def drop_definite(text, n=1):
233     """Drop definite articles 1/n of the time"""
234     return re.sub(r"\bthe\b\s*",
235                   lambda the: "" if one_in(n) else the.group(0),
236                   text,
237                   flags=re.IGNORECASE)
238
239 def replace_and(text, repl, n=1):
240     """Replace ' and ' with replacement 1/n or the time"""
241     return re.sub(r"\s*\band\b\s*",
242                   lambda et: repl if one_in(n) else et.group(0),
243                   text,
244                   flags=re.IGNORECASE)
245
246 def wrap_words(text, before, after, n=1):
247     """Wrap 1/n words in text with before and after"""
248     return re.sub(r"\w+",
249                   lambda word: wrap(word.group(0), before, after) if one_in(n) \
250                       else word.group(0),
251                   text)
252
253 ################################################################################
254 # Tests
255 ################################################################################
256
257 t1 = "The quick brown fox jumped over the lazy dog."
258
259 t2 = "Rocks and pebbles wrapped in brown paper and tied with string, then returned to nature's bosom."
260
261 t3 = "No no no there is no way that can be no..."
262
263 t4 = "notational sublimity undermining expositional transitivity"
264
265 t5 = "I see you hate being late"
266
267 ts = [t1, t2, t3, t4, t5]
268
269 def test():
270     for t in ts:
271         print repeat_run('-', 80)
272         print random_run(list('.#+-:@'), random.randint(10, 50))
273         print wrap(t, "++++", "----")
274         print replace_spaces(t, ".")
275         print all_lower(t)
276         print all_upper(t)
277         print random_upper_letters(t)
278         print random_upper_words(t)
279         print sub_1337_vowels(t)
280         print sub_1337(t, 2)
281         print sub_txt(t)
282         print sub_space_runs(t, ['.', '_'], 1, 5)
283         print drop_definite(t)
284         print replace_and(t, '+')
285         print wrap_words(t, '[', ']', 1.4)
286
287 if __name__ == "__main__":
288     test()
289
290 ################################################################################
291 # PROJECT PLAN
292 ################################################################################
293
294 #### BUGS
295
296 # FIXME: maintain case of letter and word substitutions
297
298 #### FEATURES
299
300 # Sources: file, url, url in url, rss feed
301 # BY-SA: tvtropes, Wikipedia (warn user, add license)
302 # PD: gutenberg.org
303
304 # Markov of order n
305
306 # Most frequent words from...
307 # Random text (word, words, sentence, para) from...
308
309 # tf-idf
310
311 # Generative grammars
312
313 # Pronoun swapping
314
315 # Product placement
316
317 # cut-ups
318
319 # Insert . or _ etc. randomly into words but not spaces
320 #   So 2 complimentary functions, one to replace spaces, one letters
321 # i.nternet intern_et, etc.
322
323 # Line noise
324
325 # Glitch across text
326
327 # html - bold, italic, size, face (also latex, make general)