summary refs log tree commit diff stats
path: root/sap.cpp
diff options
context:
space:
mode:
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}