summary refs log tree commit diff stats
path: root/sap.cpp
diff options
context:
space:
mode:
authorKelly Rauchenberger <fefferburbia@gmail.com>2018-01-18 16:49:54 -0500
committerKelly Rauchenberger <fefferburbia@gmail.com>2018-01-18 16:49:54 -0500
commitda3bc860f66d34f233028e819beee32dd1c43dd8 (patch)
tree8cc7f5ad0a8dad69ea5c3ae0405f803d3ba80051 /sap.cpp
parent46db0368fbee4cfba97178837e62f4469c4fa884 (diff)
downloadsap-da3bc860f66d34f233028e819beee32dd1c43dd8.tar.gz
sap-da3bc860f66d34f233028e819beee32dd1c43dd8.tar.bz2
sap-da3bc860f66d34f233028e819beee32dd1c43dd8.zip
Modernized project
This rewrite brings the codebase of this project more in line with the format of the other bots, including things like C++ randomization, better abstraction, use of exceptions, etc. Notably, any FFMPEG objects that get allocated are wrapped in simple objects so that they get properly destroyed if an exception is thrown. Some more error detection and cleanliness stuff can probably be done but my wrists really hurt.

Also updated librawr, and thus also removed the yaml-cpp submodule.
Diffstat (limited to 'sap.cpp')
-rw-r--r--sap.cpp492
1 files changed, 69 insertions, 423 deletions
diff --git a/sap.cpp b/sap.cpp index 7e1412a..f0c3fd1 100644 --- a/sap.cpp +++ b/sap.cpp
@@ -1,22 +1,9 @@
1extern "C" { 1#include "sap.h"
2#include <libavformat/avformat.h>
3#include <libavcodec/avcodec.h>
4#include <libavutil/imgutils.h>
5#include <libswscale/swscale.h>
6}
7
8#include <Magick++.h>
9#include <iostream>
10#include <rawr.h>
11#include <vector>
12#include <list>
13#include <fstream>
14#include <dirent.h>
15#include <sstream>
16#include <twitter.h>
17#include <yaml-cpp/yaml.h> 2#include <yaml-cpp/yaml.h>
18#include <thread> 3#include <thread>
19#include <chrono> 4#include <chrono>
5#include <fstream>
6#include <iostream>
20 7
21/* - random frames from Spongebob (using ffmpeg) 8/* - random frames from Spongebob (using ffmpeg)
22 * with strange text overlaid, possibly rawr'd from 9 * with strange text overlaid, possibly rawr'd from
@@ -25,277 +12,24 @@ extern "C" {
25 * frames 12 * frames
26 */ 13 */
27 14
28template <class Container> 15sap::sap(
29Container split(std::string input, std::string delimiter) 16 std::string configFile,
30{ 17 std::mt19937& rng) :
31 Container result; 18 rng_(rng)
32
33 while (!input.empty())
34 {
35 int divider = input.find(delimiter);
36 if (divider == std::string::npos)
37 {
38 result.push_back(input);
39
40 input = "";
41 } else {
42 result.push_back(input.substr(0, divider));
43
44 input = input.substr(divider+delimiter.length());
45 }
46 }
47
48 return result;
49}
50
51template <class InputIterator>
52std::string implode(InputIterator first, InputIterator last, std::string delimiter)
53{
54 std::stringstream result;
55
56 for (InputIterator it = first; it != last; it++)
57 {
58 if (it != first)
59 {
60 result << delimiter;
61 }
62
63 result << *it;
64 }
65
66 return result.str();
67}
68
69int maxWordsInLine(std::vector<std::string> words, Magick::Image& textimage)
70{
71 int result = 0;
72
73 std::string curline = "";
74 Magick::TypeMetric metric;
75 for (auto word : words)
76 {
77 curline += " " + word;
78
79 textimage.fontTypeMetrics(curline, &metric);
80 if (metric.textWidth() > ((textimage.columns()/10)*9))
81 {
82 break;
83 } else {
84 result++;
85 }
86 }
87
88 return result;
89}
90
91int minHeightRequired(std::vector<std::string> words, Magick::Image& textimage)
92{
93 int result = 0;
94 while (!words.empty())
95 {
96 int prefixlen = maxWordsInLine(words, textimage);
97 std::string prefixText = implode(std::begin(words), std::begin(words) + prefixlen, " ");
98 std::vector<std::string> suffix(std::begin(words) + prefixlen, std::end(words));
99 Magick::TypeMetric metric;
100 textimage.fontTypeMetrics(prefixText, &metric);
101 result += metric.textHeight() + 5;
102
103 words = suffix;
104 }
105
106 return result - 5;
107}
108
109void layoutText(Magick::Image& textimage, Magick::Image& shadowimage, int width, int height, std::string text, const std::vector<std::string>& fonts)
110{
111 textimage.fillColor(Magick::Color(MaxRGB, MaxRGB, MaxRGB, MaxRGB * 0.0));
112 shadowimage.fillColor(Magick::Color(0, 0, 0, 0));
113 shadowimage.strokeColor("black");
114
115 int minSize = 48;
116 int realMaxSize = 96;
117 int maxSize = realMaxSize;
118 Magick::TypeMetric metric;
119 std::string font;
120 auto words = split<std::vector<std::string>>(text, " ");
121 int top = 5;
122 int minWords = 1;
123 while (!words.empty())
124 {
125 if (font.empty() || (rand() % 10 == 0))
126 {
127 font = fonts[rand() % fonts.size()];
128 textimage.font(font);
129 shadowimage.font(font);
130 }
131
132 int size = rand() % (maxSize - minSize + 1) + minSize;
133 textimage.fontPointsize(size);
134 int maxWords = maxWordsInLine(words, textimage);
135 int touse;
136 if (minWords > maxWords)
137 {
138 touse = maxWords;
139 } else {
140 touse = rand() % (maxWords - minWords + 1) + minWords;
141 }
142 std::string prefixText = implode(std::begin(words), std::begin(words) + touse, " ");
143 std::vector<std::string> suffix(std::begin(words) + touse, std::end(words));
144 textimage.fontTypeMetrics(prefixText, &metric);
145
146 textimage.fontPointsize(minSize);
147 int lowpadding = minHeightRequired(suffix, textimage);
148 int freespace = height - 5 - top - lowpadding - metric.textHeight();
149 std::cout << "top of " << top << " with lowpad of " << lowpadding << " and textheight of " << metric.textHeight() << " with freespace=" << freespace << std::endl;
150 if (freespace < 0)
151 {
152 minWords = touse;
153
154 continue;
155 }
156
157 maxSize = realMaxSize;
158 minWords = 1;
159
160 int toppadding;
161 if (rand() % 2 == 0)
162 {
163 // Exponential distribution, biased toward top
164 toppadding = log(rand() % (int)exp(freespace + 1) + 1);
165 } else {
166 // Linear distribution, biased toward bottom
167 toppadding = rand() % (freespace + 1);
168 }
169
170 int leftx = rand() % (width - 10 - (int)metric.textWidth()) + 5;
171 std::cout << "printing at " << leftx << "," << (top + toppadding + metric.ascent()) << std::endl;
172 textimage.fontPointsize(size);
173 textimage.annotate(prefixText, Magick::Geometry(0, 0, leftx, top + toppadding + metric.ascent()));
174
175 shadowimage.fontPointsize(size);
176 shadowimage.strokeWidth(size / 10);
177 shadowimage.annotate(prefixText, Magick::Geometry(0, 0, leftx, top + toppadding + metric.ascent()));
178 //shadowimage.draw(Magick::DrawableRectangle(leftx - 5, top + toppadding, leftx + metric.textWidth() + 5, top + toppadding + metric.textHeight() + 10 + metric.descent()));
179
180 words = suffix;
181 top += toppadding + metric.textHeight();
182 }
183
184 Magick::PixelPacket* shadowpixels = shadowimage.getPixels(0, 0, width, height);
185 Magick::PixelPacket* textpixels = textimage.getPixels(0, 0, width, height);
186 for (int j=0; j<height; j++)
187 {
188 for (int i=0; i<width; i++)
189 {
190 int ind = j*width+i;
191 if (shadowpixels[ind].opacity != MaxRGB)
192 {
193 shadowpixels[ind].opacity = MaxRGB * 0.25;
194 }
195
196 if (textpixels[ind].opacity != MaxRGB)
197 {
198 //textpixels[ind].opacity = MaxRGB * 0.05;
199 }
200 }
201 }
202
203 shadowimage.syncPixels();
204 textimage.syncPixels();
205
206 shadowimage.blur(10.0, 20.0);
207 textimage.blur(0.0, 0.5);
208}
209
210static int open_codec_context(int *stream_idx, AVFormatContext *fmt_ctx, enum AVMediaType type)
211{
212 int ret, stream_index;
213 AVStream *st;
214 AVCodecContext *dec_ctx = NULL;
215 AVCodec *dec = NULL;
216 AVDictionary *opts = NULL;
217 ret = av_find_best_stream(fmt_ctx, type, -1, -1, NULL, 0);
218 if (ret < 0)
219 {
220 //fprintf(stderr, "Could not find %s stream in input file '%s'\n", av_get_media_type_string(type), src_filename);
221 return ret;
222 } else {
223 stream_index = ret;
224 st = fmt_ctx->streams[stream_index];
225
226 // find decoder for the stream
227 dec_ctx = st->codec;
228 dec = avcodec_find_decoder(dec_ctx->codec_id);
229 if (!dec)
230 {
231 fprintf(stderr, "Failed to find %s codec\n", av_get_media_type_string(type));
232 return AVERROR(EINVAL);
233 }
234
235 // Init the decoders, with or without reference counting
236 av_dict_set(&opts, "refcounted_frames", "0", 0);
237 if ((ret = avcodec_open2(dec_ctx, dec, &opts)) < 0)
238 {
239 fprintf(stderr, "Failed to open %s codec\n", av_get_media_type_string(type));
240 return ret;
241 }
242
243 *stream_idx = stream_index;
244 }
245
246 return 0;
247}
248
249int main(int argc, char** argv)
250{ 19{
251 srand(time(NULL)); 20 // Load the config file.
252 rand(); rand(); rand(); rand(); 21 YAML::Node config = YAML::LoadFile(configFile);
253
254 Magick::InitializeMagick(nullptr);
255 av_register_all();
256 22
257 if (argc != 2) 23 // Set up the Twitter client.
258 {
259 std::cout << "usage: sap [configfile]" << std::endl;
260 return -1;
261 }
262
263 std::string configfile(argv[1]);
264 YAML::Node config = YAML::LoadFile(configfile);
265
266 twitter::auth auth; 24 twitter::auth auth;
267 auth.setConsumerKey(config["consumer_key"].as<std::string>()); 25 auth.setConsumerKey(config["consumer_key"].as<std::string>());
268 auth.setConsumerSecret(config["consumer_secret"].as<std::string>()); 26 auth.setConsumerSecret(config["consumer_secret"].as<std::string>());
269 auth.setAccessKey(config["access_key"].as<std::string>()); 27 auth.setAccessKey(config["access_key"].as<std::string>());
270 auth.setAccessSecret(config["access_secret"].as<std::string>()); 28 auth.setAccessSecret(config["access_secret"].as<std::string>());
271
272 twitter::client client(auth);
273
274 // Fonts
275 std::vector<std::string> fonts;
276 {
277 std::string fontdirname = config["fonts"].as<std::string>();
278 DIR* fontdir;
279 struct dirent* ent;
280 if ((fontdir = opendir(fontdirname.c_str())) == nullptr)
281 {
282 std::cout << "Couldn't find fonts." << std::endl;
283 return -2;
284 }
285 29
286 while ((ent = readdir(fontdir)) != nullptr) 30 client_ = std::unique_ptr<twitter::client>(new twitter::client(auth));
287 {
288 std::string dname(ent->d_name);
289 if ((dname.find(".otf") != std::string::npos) || (dname.find(".ttf") != std::string::npos))
290 {
291 fonts.push_back(fontdirname + "/" + dname);
292 }
293 }
294 31
295 closedir(fontdir); 32 // Set up the text generator.
296 }
297
298 rawr kgramstats;
299 for (const YAML::Node& corpusname : config["corpuses"]) 33 for (const YAML::Node& corpusname : config["corpuses"])
300 { 34 {
301 std::ifstream infile(corpusname.as<std::string>()); 35 std::ifstream infile(corpusname.as<std::string>());
@@ -307,164 +41,76 @@ int main(int argc, char** argv)
307 { 41 {
308 line.pop_back(); 42 line.pop_back();
309 } 43 }
310 44
311 corpus += line + " "; 45 corpus += line + " ";
312 } 46 }
313
314 kgramstats.addCorpus(corpus);
315 }
316 47
317 kgramstats.compile(5); 48 kgramstats_.addCorpus(corpus);
318 kgramstats.setMinCorpora(config["corpuses"].size());
319
320 std::string videodirname = config["videos"].as<std::string>();
321 DIR* videodir;
322 struct dirent* ent;
323 if ((videodir = opendir(videodirname.c_str())) == nullptr)
324 {
325 std::cout << "Couldn't find videos." << std::endl;
326 return -1;
327 } 49 }
328 50
329 std::vector<std::string> videos; 51 kgramstats_.compile(5);
330 while ((ent = readdir(videodir)) != nullptr) 52 kgramstats_.setMinCorpora(config["corpuses"].size());
331 {
332 std::string dname(ent->d_name);
333 if (dname.find(".mp4") != std::string::npos)
334 {
335 videos.push_back(dname);
336 }
337 }
338 53
339 closedir(videodir); 54 // Set up the layout designer.
340 55 layout_ = std::unique_ptr<designer>(
56 new designer(config["fonts"].as<std::string>()));
57
58 // Set up the frame picker.
59 director_ = std::unique_ptr<director>(
60 new director(config["videos"].as<std::string>()));
61}
62
63void sap::run() const
64{
341 for (;;) 65 for (;;)
342 { 66 {
343 std::string video = videodirname + "/" + videos[rand() % videos.size()]; 67 std::cout << "Generating tweet..." << std::endl;
344 std::cout << "Opening " << video << std::endl;
345
346 AVFormatContext* format = nullptr;
347 if (avformat_open_input(&format, video.c_str(), nullptr, nullptr))
348 {
349 std::cout << "could not open file" << std::endl;
350 return 1;
351 }
352
353 if (avformat_find_stream_info(format, nullptr))
354 {
355 std::cout << "could not read stream" << std::endl;
356 return 5;
357 }
358
359 int video_stream_idx = -1;
360 if (open_codec_context(&video_stream_idx, format, AVMEDIA_TYPE_VIDEO))
361 {
362 std::cout << "could not open codec" << std::endl;
363 return 6;
364 }
365
366 AVStream* stream = format->streams[video_stream_idx];
367 AVCodecContext* codec = stream->codec;
368 int codecw = codec->width;
369 int codech = codec->height;
370 68
371 int64_t seek = (rand() % format->duration) * codec->time_base.num / codec->time_base.den; 69 try
372 std::cout << seek << std::endl;
373 if (av_seek_frame(format, video_stream_idx, seek, 0))
374 { 70 {
375 std::cout << "could not seek" << std::endl; 71 // Pick the video frame.
376 return 4; 72 Magick::Image image = director_->generate(rng_);
377 } 73
378 74 // Generate the text.
379 AVPacket packet; 75 std::uniform_int_distribution<size_t> lenDist(5, 19);
380 av_init_packet(&packet); 76 std::string action = kgramstats_.randomSentence(lenDist(rng_));
381
382 AVFrame* frame = av_frame_alloc();
383 AVFrame* converted = av_frame_alloc();
384
385 int buffer_size = av_image_get_buffer_size(AV_PIX_FMT_RGB24, codecw, codech, 1);
386 uint8_t* buffer = new uint8_t[buffer_size];
387 77
388 av_image_alloc(converted->data, converted->linesize, codecw, codech, AV_PIX_FMT_RGB24, 1); 78 // Lay the text on the video frame.
79 Magick::Image textimage =
80 layout_->generate(image.columns(), image.rows(), action, rng_);
81 image.composite(textimage, 0, 0, Magick::OverCompositeOp);
389 82
390 for (;;) 83 // Send the tweet.
84 std::cout << "Sending tweet..." << std::endl;
85
86 sendTweet(std::move(image));
87
88 std::cout << "Tweeted!" << std::endl;
89
90 // Wait.
91 std::this_thread::sleep_for(std::chrono::hours(1));
92 } catch (const Magick::ErrorImage& ex)
391 { 93 {
392 if (av_read_frame(format, &packet)) 94 std::cout << "Image error: " << ex.what() << std::endl;
393 { 95 } catch (const twitter::twitter_error& ex)
394 std::cout << "could not read frame" << std::endl; 96 {
395 return 2; 97 std::cout << "Twitter error: " << ex.what() << std::endl;
396 } 98
397 99 std::this_thread::sleep_for(std::chrono::hours(1));
398 if (packet.stream_index != video_stream_idx)
399 {
400 continue;
401 }
402
403 int got_pic;
404 if (avcodec_decode_video2(codec, frame, &got_pic, &packet) < 0)
405 {
406 std::cout << "could not decode frame" << std::endl;
407 return 7;
408 }
409
410 if (!got_pic)
411 {
412 continue;
413 }
414
415 if (packet.flags && AV_PKT_FLAG_KEY)
416 {
417 SwsContext* sws = sws_getContext(codecw, codech, codec->pix_fmt, codecw, codech, AV_PIX_FMT_RGB24, 0, nullptr, nullptr, 0);
418 sws_scale(sws, frame->data, frame->linesize, 0, codech, converted->data, converted->linesize);
419 sws_freeContext(sws);
420
421 av_image_copy_to_buffer(buffer, buffer_size, converted->data, converted->linesize, AV_PIX_FMT_RGB24, codecw, codech, 1);
422 av_frame_free(&frame);
423 av_frame_free(&converted);
424 av_packet_unref(&packet);
425 avcodec_close(codec);
426 avformat_close_input(&format);
427
428 int width = 1024;
429 int height = codech * width / codecw;
430
431 Magick::Image image;
432 image.read(codecw, codech, "RGB", Magick::CharPixel, buffer);
433 image.zoom(Magick::Geometry(width, height));
434
435 std::string action = kgramstats.randomSentence(rand() % 15 + 5);
436 Magick::Image textimage(Magick::Geometry(width, height), "transparent");
437 Magick::Image shadowimage(Magick::Geometry(width, height), "transparent");
438 layoutText(textimage, shadowimage, width, height, action, fonts);
439 image.composite(shadowimage, 0, 0, Magick::OverCompositeOp);
440 image.composite(textimage, 0, 0, Magick::OverCompositeOp);
441
442 image.magick("jpeg");
443
444 Magick::Blob outputimg;
445 image.write(&outputimg);
446
447 delete[] buffer;
448
449 std::cout << "Generated image." << std::endl << "Tweeting..." << std::endl;
450
451 try
452 {
453 long media_id = client.uploadMedia("image/jpeg", (const char*) outputimg.data(), outputimg.length());
454 client.updateStatus("", {media_id});
455
456 std::cout << "Done!" << std::endl << "Waiting..." << std::endl << std::endl;
457 } catch (const twitter::twitter_error& error)
458 {
459 std::cout << "Twitter error: " << error.what() << std::endl;
460 }
461
462 break;
463 }
464 } 100 }
465 101
466 std::this_thread::sleep_for(std::chrono::hours(1)); 102 std::cout << std::endl;
467 } 103 }
468 104}
469 return 0; 105
106void sap::sendTweet(Magick::Image image) const
107{
108 Magick::Blob outputimg;
109 image.magick("jpeg");
110 image.write(&outputimg);
111
112 long media_id = client_->uploadMedia("image/jpeg",
113 static_cast<const char*>(outputimg.data()), outputimg.length());
114
115 client_->updateStatus("", {media_id});
470} 116}