summary refs log tree commit diff stats
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
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.
-rw-r--r--.gitignore5
-rw-r--r--CMakeLists.txt28
-rw-r--r--designer.cpp244
-rw-r--r--designer.h40
-rw-r--r--director.cpp378
-rw-r--r--director.h31
-rw-r--r--main.cpp45
-rw-r--r--sap.cpp492
-rw-r--r--sap.h34
-rw-r--r--util.h60
m---------vendor/rawr-ebooks0
11 files changed, 926 insertions, 431 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e2b3f04 --- /dev/null +++ b/.gitignore
@@ -0,0 +1,5 @@
1.DS_Store
2CMakeFiles
3CMakeCache.txt
4cmake_install.cmake
5Makefile
diff --git a/CMakeLists.txt b/CMakeLists.txt index 70a9572..efafdaf 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt
@@ -1,22 +1,34 @@
1cmake_minimum_required (VERSION 3.1) 1cmake_minimum_required (VERSION 3.1)
2project (sap) 2project (sap)
3 3
4set(CMAKE_BUILD_TYPE Debug)
5
6set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_SOURCE_DIR}/cmake/") 4set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_SOURCE_DIR}/cmake/")
7 5
8find_package(FFMPEG REQUIRED) 6find_package(FFMPEG REQUIRED)
9include_directories(${FFMPEG_INCLUDE_DIRS})
10 7
11find_package(PkgConfig) 8find_package(PkgConfig)
9pkg_check_modules(yaml-cpp yaml-cpp REQUIRED)
12pkg_check_modules(GraphicsMagick GraphicsMagick++ REQUIRED) 10pkg_check_modules(GraphicsMagick GraphicsMagick++ REQUIRED)
13include_directories(${GraphicsMagick_INCLUDE_DIRS})
14link_directories(${GraphicsMagick_LIBRARY_DIRS})
15 11
16add_subdirectory(vendor/rawr-ebooks EXCLUDE_FROM_ALL) 12add_subdirectory(vendor/rawr-ebooks EXCLUDE_FROM_ALL)
17include_directories(vendor/rawr-ebooks vendor/rawr-ebooks/vendor/libtwittercpp/src vendor/rawr-ebooks/vendor/yaml-cpp/include)
18 13
19add_executable(sap sap.cpp) 14include_directories(
15 ${FFMPEG_INCLUDE_DIRS}
16 vendor/rawr-ebooks
17 vendor/rawr-ebooks/vendor/libtwittercpp/src
18 ${yaml-cpp_INCLUDE_DIRS}
19 ${GraphicsMagick_INCLUDE_DIRS})
20
21link_directories(
22 ${yaml-cpp_LIBRARY_DIRS}
23 ${GraphicsMagick_LIBRARY_DIRS})
24
25add_executable(sap sap.cpp director.cpp designer.cpp main.cpp)
20set_property(TARGET sap PROPERTY CXX_STANDARD 11) 26set_property(TARGET sap PROPERTY CXX_STANDARD 11)
21set_property(TARGET sap PROPERTY CXX_STANDARD_REQUIRED ON) 27set_property(TARGET sap PROPERTY CXX_STANDARD_REQUIRED ON)
22target_link_libraries(sap rawr twitter++ ${FFMPEG_LIBRARIES} ${GraphicsMagick_LIBRARIES} yaml-cpp) 28
29target_link_libraries(sap
30 rawr
31 twitter++
32 ${FFMPEG_LIBRARIES}
33 ${GraphicsMagick_LIBRARIES}
34 ${yaml-cpp_LIBRARIES})
diff --git a/designer.cpp b/designer.cpp new file mode 100644 index 0000000..3bdcbeb --- /dev/null +++ b/designer.cpp
@@ -0,0 +1,244 @@
1#include "designer.h"
2#include <dirent.h>
3#include <stdexcept>
4#include <iostream>
5#include "util.h"
6
7designer::designer(std::string fontsDir)
8{
9 DIR* fontdir;
10 struct dirent* ent;
11 if ((fontdir = opendir(fontsDir.c_str())) == nullptr)
12 {
13 throw std::invalid_argument("Couldn't find fonts");
14 }
15
16 while ((ent = readdir(fontdir)) != nullptr)
17 {
18 std::string dname(ent->d_name);
19 if ((dname.find(".otf") != std::string::npos)
20 || (dname.find(".ttf") != std::string::npos))
21 {
22 fonts.push_back(fontsDir + "/" + dname);
23 }
24 }
25
26 closedir(fontdir);
27}
28
29Magick::Image designer::generate(
30 size_t width,
31 size_t height,
32 const std::string& text,
33 std::mt19937& rng) const
34{
35 // Initialize two layers: the text, and a shadow to increase contrast.
36 Magick::Image textimage(Magick::Geometry(width, height), "transparent");
37 Magick::Image shadowimage(Magick::Geometry(width, height), "transparent");
38
39 textimage.fillColor(Magick::Color(MaxRGB, MaxRGB, MaxRGB, MaxRGB * 0.0));
40 shadowimage.fillColor(Magick::Color(0, 0, 0, 0));
41 shadowimage.strokeColor("black");
42
43 bool hasFont = false;
44 std::uniform_int_distribution<size_t> fontDist(0, fonts.size() - 1);
45
46 std::vector<std::string> words = split<std::vector<std::string>>(text, " ");
47 std::vector<std::string>::const_iterator curWord = std::begin(words);
48
49 size_t top = V_PADDING;
50 size_t minWords = 1;
51 while (curWord != std::end(words))
52 {
53 // There is a 1 in 10 chance of randomly changing the font. We also have to
54 // set the font if this is the beginning of generation.
55 if (!hasFont || (std::bernoulli_distribution(0.1)(rng)))
56 {
57 std::string font = fonts[fontDist(rng)];
58
59 textimage.font(font);
60 shadowimage.font(font);
61
62 hasFont = true;
63 }
64
65 // Choose a font size for the current line.
66 std::uniform_int_distribution<size_t> sizeDist(
67 MIN_FONT_SIZE, MAX_FONT_SIZE);
68
69 size_t size = sizeDist(rng);
70 textimage.fontPointsize(size);
71
72 // Decide what words to put on this line.
73 size_t maxWords = maxWordsInLine(curWord, std::end(words), textimage);
74
75 size_t lineLen;
76 if (minWords > maxWords)
77 {
78 lineLen = maxWords;
79 } else {
80 std::uniform_int_distribution<size_t> lenDist(minWords, maxWords);
81 lineLen = lenDist(rng);
82 }
83
84 std::string prefixText = implode(curWord, curWord + lineLen, " ");
85
86 // Determine if the choice of font, font size, and number of words, would
87 // prevent the algorithm from being about to layout the entire string even
88 // if the rest of it were rendered in the smallest font size.
89 Magick::TypeMetric metric;
90 textimage.fontTypeMetrics(prefixText, &metric);
91
92 textimage.fontPointsize(MIN_FONT_SIZE);
93
94 // This is how much vertical space would be required to render the rest of
95 // the string at the minimum font size.
96 int lowpadding = minHeightRequired(
97 curWord + lineLen,
98 std::end(words),
99 textimage);
100
101 // This is the amount of space that would be left over if the rest of the
102 // string were rendered at the minimum font size.
103 int freespace =
104 static_cast<int>(height) - static_cast<int>(V_PADDING)
105 - static_cast<int>(top) - lowpadding - metric.textHeight();
106
107 // Some debug text.
108 std::cout << "top of " << top << " with lowpad of " << lowpadding
109 << " and textheight of " << metric.textHeight() << " with freespace="
110 << freespace << std::endl;
111
112 // If there wouldn't be enough room to render the rest of the string at the
113 // minimum font size, go back to the top of the loop and choose a different
114 // font, font size, and number of words.
115 if (freespace < 0)
116 {
117 minWords = lineLen;
118
119 continue;
120 }
121
122 minWords = 1;
123
124 // Determine how much space to leave between this line and the previous one.
125 size_t toppadding;
126 if (std::bernoulli_distribution(0.5)(rng))
127 {
128 // Exponential distribution, biased toward top
129 std::uniform_int_distribution<size_t> expDist(
130 1, static_cast<size_t>(exp(freespace + 1)));
131
132 toppadding = log(expDist(rng));
133 } else {
134 // Linear distribution, biased toward bottom
135 std::uniform_int_distribution<size_t> linDist(0, freespace);
136
137 toppadding = linDist(rng);
138 }
139
140 // Determine the x-coordinate of this line.
141 std::uniform_int_distribution<size_t> leftDist(
142 H_PADDING,
143 width - H_PADDING - static_cast<size_t>(metric.textWidth()) - 1);
144
145 size_t leftx = leftDist(rng);
146
147 // Render this line.
148 size_t ycor = top + toppadding + metric.ascent();
149
150 std::cout << "printing at " << leftx << "," << ycor << std::endl;
151
152 textimage.fontPointsize(size);
153 textimage.annotate(prefixText, Magick::Geometry(0, 0, leftx, ycor));
154
155 shadowimage.fontPointsize(size);
156 shadowimage.strokeWidth(size / 10);
157 shadowimage.annotate(prefixText, Magick::Geometry(0, 0, leftx, ycor));
158
159 top += toppadding + metric.textHeight();
160
161 std::advance(curWord, lineLen);
162 }
163
164 // Make the shadow layer semi-transparent.
165 Magick::PixelPacket* shadpixels = shadowimage.getPixels(0, 0, width, height);
166 Magick::PixelPacket* textpixels = textimage.getPixels(0, 0, width, height);
167 for (size_t j = 0; j < height; j++)
168 {
169 for (size_t i = 0; i < width; i++)
170 {
171 size_t ind = j * width + i;
172
173 if (shadpixels[ind].opacity != MaxRGB)
174 {
175 shadpixels[ind].opacity = MaxRGB * 0.25;
176 }
177 }
178 }
179
180 shadowimage.syncPixels();
181 textimage.syncPixels();
182
183 // Add some blur to the text and shadow.
184 shadowimage.blur(10.0, 20.0);
185 textimage.blur(0.0, 0.5);
186
187 // Put the text layer on top of the shadow.
188 shadowimage.composite(textimage, 0, 0, Magick::OverCompositeOp);
189
190 return shadowimage;
191}
192
193size_t designer::maxWordsInLine(
194 std::vector<std::string>::const_iterator first,
195 std::vector<std::string>::const_iterator last,
196 Magick::Image& textimage) const
197{
198 size_t result = 0;
199
200 std::string curline = "";
201 for (; first != last; first++)
202 {
203 curline += " " + *first;
204
205 Magick::TypeMetric metric;
206 textimage.fontTypeMetrics(curline, &metric);
207
208 if (metric.textWidth() > ((textimage.columns() / 10) * 9))
209 {
210 break;
211 } else {
212 result++;
213 }
214 }
215
216 return result;
217}
218
219size_t designer::minHeightRequired(
220 std::vector<std::string>::const_iterator first,
221 std::vector<std::string>::const_iterator last,
222 Magick::Image& textimage) const
223{
224 if (first == last)
225 {
226 return 0;
227 } else {
228 size_t result = 0;
229
230 while (first != last)
231 {
232 size_t prefixlen = maxWordsInLine(first, last, textimage);
233 std::string prefixText = implode(first, first + prefixlen, " ");
234
235 Magick::TypeMetric metric;
236 textimage.fontTypeMetrics(prefixText, &metric);
237 result += metric.textHeight() + V_PADDING;
238
239 std::advance(first, prefixlen);
240 }
241
242 return result - V_PADDING;
243 }
244}
diff --git a/designer.h b/designer.h new file mode 100644 index 0000000..6a348a0 --- /dev/null +++ b/designer.h
@@ -0,0 +1,40 @@
1#ifndef DESIGNER_H_CCE34BEB
2#define DESIGNER_H_CCE34BEB
3
4#include <random>
5#include <string>
6#include <Magick++.h>
7#include <vector>
8
9class designer {
10public:
11
12 designer(std::string fontsPath);
13
14 Magick::Image generate(
15 size_t width,
16 size_t height,
17 const std::string& text,
18 std::mt19937& rng) const;
19
20private:
21
22 const size_t MIN_FONT_SIZE = 48;
23 const size_t MAX_FONT_SIZE = 96;
24 const size_t V_PADDING = 5;
25 const size_t H_PADDING = 5;
26
27 size_t maxWordsInLine(
28 std::vector<std::string>::const_iterator first,
29 std::vector<std::string>::const_iterator last,
30 Magick::Image& textimage) const;
31
32 size_t minHeightRequired(
33 std::vector<std::string>::const_iterator first,
34 std::vector<std::string>::const_iterator last,
35 Magick::Image& textimage) const;
36
37 std::vector<std::string> fonts;
38};
39
40#endif /* end of include guard: DESIGNER_H_CCE34BEB */
diff --git a/director.cpp b/director.cpp new file mode 100644 index 0000000..24335da --- /dev/null +++ b/director.cpp
@@ -0,0 +1,378 @@
1#include "director.h"
2#include <dirent.h>
3#include <iostream>
4
5extern "C" {
6#include <libavformat/avformat.h>
7#include <libavcodec/avcodec.h>
8#include <libavutil/imgutils.h>
9#include <libswscale/swscale.h>
10}
11
12namespace ffmpeg {
13
14 class format {
15 public:
16
17 format(std::string videoPath)
18 {
19 if (avformat_open_input(&ptr_, videoPath.c_str(), nullptr, nullptr))
20 {
21 throw ffmpeg_error("Could not open video " + videoPath);
22 }
23
24 if (avformat_find_stream_info(ptr_, nullptr))
25 {
26 throw ffmpeg_error("Could not read stream");
27 }
28 }
29
30 ~format()
31 {
32 avformat_close_input(&ptr_);
33 }
34
35 format(const format& other) = delete;
36
37 AVFormatContext* ptr()
38 {
39 return ptr_;
40 }
41
42 private:
43
44 AVFormatContext* ptr_ = nullptr;
45 };
46
47 class codec {
48 public:
49
50 codec(
51 format& fmt,
52 AVStream* st)
53 {
54 AVCodec* dec = avcodec_find_decoder(st->codecpar->codec_id);
55 if (!dec)
56 {
57 throw ffmpeg_error("Failed to find codec");
58 }
59
60 ptr_ = avcodec_alloc_context3(dec);
61 if (!ptr_)
62 {
63 throw ffmpeg_error("Failed to allocate codec context");
64 }
65
66 if (avcodec_parameters_to_context(ptr_, st->codecpar) < 0)
67 {
68 throw ffmpeg_error("Failed to copy codec parameters to decoder");
69 }
70
71 // Init the decoders, with or without reference counting
72 AVDictionary* opts = nullptr;
73 av_dict_set(&opts, "refcounted_frames", "0", 0);
74
75 if (avcodec_open2(ptr_, dec, &opts) < 0)
76 {
77 throw ffmpeg_error("Failed to open codec");
78 }
79 }
80
81 codec(const codec& other) = delete;
82
83 ~codec()
84 {
85 avcodec_free_context(&ptr_);
86 }
87
88 int getWidth() const
89 {
90 return ptr_->width;
91 }
92
93 int getHeight() const
94 {
95 return ptr_->height;
96 }
97
98 enum AVPixelFormat getPixelFormat() const
99 {
100 return ptr_->pix_fmt;
101 }
102
103 AVCodecContext* ptr()
104 {
105 return ptr_;
106 }
107
108 private:
109
110 AVCodecContext* ptr_ = nullptr;
111 };
112
113 class packet {
114 public:
115
116 packet()
117 {
118 ptr_ = av_packet_alloc();
119 }
120
121 ~packet()
122 {
123 av_packet_free(&ptr_);
124 }
125
126 packet(const packet& other) = delete;
127
128 int getStreamIndex() const
129 {
130 return ptr_->stream_index;
131 }
132
133 AVPacket* ptr()
134 {
135 return ptr_;
136 }
137
138 int getFlags() const
139 {
140 return ptr_->flags;
141 }
142
143 private:
144
145 AVPacket* ptr_ = nullptr;
146 };
147
148 class frame {
149 public:
150
151 frame()
152 {
153 ptr_ = av_frame_alloc();
154 }
155
156 ~frame()
157 {
158 av_frame_free(&ptr_);
159 }
160
161 frame(const frame& other) = delete;
162
163 uint8_t** getData()
164 {
165 return ptr_->data;
166 }
167
168 int* getLinesize()
169 {
170 return ptr_->linesize;
171 }
172
173 AVFrame* ptr()
174 {
175 return ptr_;
176 }
177
178 private:
179
180 AVFrame* ptr_ = nullptr;
181 };
182
183 class sws {
184 public:
185
186 sws(
187 int srcW,
188 int srcH,
189 enum AVPixelFormat srcFormat,
190 int dstW,
191 int dstH,
192 enum AVPixelFormat dstFormat,
193 int flags,
194 SwsFilter* srcFilter,
195 SwsFilter* dstFilter,
196 const double* param)
197 {
198 ptr_ = sws_getContext(
199 srcW,
200 srcH,
201 srcFormat,
202 dstW,
203 dstH,
204 dstFormat,
205 flags,
206 srcFilter,
207 dstFilter,
208 param);
209
210 if (ptr_ == NULL)
211 {
212 throw ffmpeg_error("Could not allocate sws context");
213 }
214 }
215
216 ~sws()
217 {
218 sws_freeContext(ptr_);
219 }
220
221 sws(const sws& other) = delete;
222
223 void scale(
224 const uint8_t* const srcSlice[],
225 const int srcStride[],
226 int srcSliceY,
227 int srcSliceH,
228 uint8_t* const dst[],
229 const int dstStride[])
230 {
231 sws_scale(
232 ptr_,
233 srcSlice,
234 srcStride,
235 srcSliceY,
236 srcSliceH,
237 dst,
238 dstStride);
239 }
240
241 private:
242
243 SwsContext* ptr_;
244 };
245
246}
247
248director::director(std::string videosPath) : videosPath_(videosPath)
249{
250 DIR* videodir;
251 struct dirent* ent;
252 if ((videodir = opendir(videosPath.c_str())) == nullptr)
253 {
254 throw std::invalid_argument("Couldn't find videos");
255 }
256
257 while ((ent = readdir(videodir)) != nullptr)
258 {
259 std::string dname(ent->d_name);
260 if (dname.find(".mp4") != std::string::npos)
261 {
262 videos_.push_back(dname);
263 }
264 }
265
266 closedir(videodir);
267}
268
269Magick::Image director::generate(std::mt19937& rng) const
270{
271 std::uniform_int_distribution<size_t> videoDist(0, videos_.size() - 1);
272 std::string video = videosPath_ + "/" + videos_[videoDist(rng)];
273
274 ffmpeg::format fmt(video);
275
276 int streamIdx =
277 av_find_best_stream(fmt.ptr(), AVMEDIA_TYPE_VIDEO, -1, -1, NULL, 0);
278
279 if (streamIdx < 0)
280 {
281 throw ffmpeg_error("Could not find stream");
282 }
283
284 AVStream* stream = fmt.ptr()->streams[streamIdx];
285
286 ffmpeg::codec cdc(fmt, stream);
287
288 size_t codecw = cdc.getWidth();
289 size_t codech = cdc.getHeight();
290
291 std::uniform_int_distribution<int64_t> frameDist(0, stream->duration - 1);
292 int64_t seek = frameDist(rng);
293
294 std::cout << seek << std::endl;
295
296 if (av_seek_frame(fmt.ptr(), streamIdx, seek, 0))
297 {
298 throw ffmpeg_error("Could not seek");
299 }
300
301 ffmpeg::frame frame;
302 ffmpeg::frame converted;
303
304 av_image_alloc(
305 converted.getData(),
306 converted.getLinesize(),
307 codecw,
308 codech,
309 AV_PIX_FMT_RGB24,
310 1);
311
312 ffmpeg::packet pkt;
313
314 do
315 {
316 if (av_read_frame(fmt.ptr(), pkt.ptr()) < 0)
317 {
318 throw ffmpeg_error("Could not read frame");
319 }
320 } while ((pkt.getStreamIndex() != streamIdx)
321 || !(pkt.getFlags() & AV_PKT_FLAG_KEY));
322
323 int ret;
324 do {
325 if (avcodec_send_packet(cdc.ptr(), pkt.ptr()) < 0)
326 {
327 throw ffmpeg_error("Could not send packet");
328 }
329
330 ret = avcodec_receive_frame(cdc.ptr(), frame.ptr());
331 } while (ret == AVERROR(EAGAIN));
332
333 if (ret < 0)
334 {
335 throw ffmpeg_error("Could not decode frame");
336 }
337
338 ffmpeg::sws scaler(
339 cdc.getWidth(),
340 cdc.getHeight(),
341 cdc.getPixelFormat(),
342 cdc.getWidth(),
343 cdc.getHeight(),
344 AV_PIX_FMT_RGB24,
345 0, nullptr, nullptr, 0);
346
347 scaler.scale(
348 frame.getData(),
349 frame.getLinesize(),
350 0,
351 codech,
352 converted.getData(),
353 converted.getLinesize());
354
355 size_t buffer_size = av_image_get_buffer_size(
356 AV_PIX_FMT_RGB24, cdc.getWidth(), cdc.getHeight(), 1);
357
358 std::vector<uint8_t> buffer(buffer_size);
359
360 av_image_copy_to_buffer(
361 buffer.data(),
362 buffer_size,
363 converted.getData(),
364 converted.getLinesize(),
365 AV_PIX_FMT_RGB24,
366 cdc.getWidth(),
367 cdc.getHeight(),
368 1);
369
370 size_t width = 1024;
371 size_t height = codech * width / codecw;
372
373 Magick::Image image;
374 image.read(codecw, codech, "RGB", Magick::CharPixel, buffer.data());
375 image.zoom(Magick::Geometry(width, height));
376
377 return image;
378}
diff --git a/director.h b/director.h new file mode 100644 index 0000000..f00f2ee --- /dev/null +++ b/director.h
@@ -0,0 +1,31 @@
1#ifndef DIRECTOR_H_9DFD929C
2#define DIRECTOR_H_9DFD929C
3
4#include <stdexcept>
5#include <string>
6#include <vector>
7#include <Magick++.h>
8#include <random>
9
10class ffmpeg_error : public std::runtime_error {
11public:
12
13 ffmpeg_error(std::string msg) : std::runtime_error(msg)
14 {
15 }
16};
17
18class director {
19public:
20
21 director(std::string videosPath);
22
23 Magick::Image generate(std::mt19937& rng) const;
24
25private:
26
27 std::string videosPath_;
28 std::vector<std::string> videos_;
29};
30
31#endif /* end of include guard: DIRECTOR_H_9DFD929C */
diff --git a/main.cpp b/main.cpp new file mode 100644 index 0000000..6ac8ee9 --- /dev/null +++ b/main.cpp
@@ -0,0 +1,45 @@
1#include "sap.h"
2#include <iostream>
3#include <ctime>
4#include <cstdlib>
5
6extern "C" {
7#include <libavformat/avformat.h>
8}
9
10int main(int argc, char** argv)
11{
12 Magick::InitializeMagick(nullptr);
13 av_register_all();
14
15 std::random_device randomDevice;
16 std::mt19937 rng(randomDevice());
17
18 // We also have to seed the C-style RNG because librawr uses it.
19 srand(time(NULL));
20 rand(); rand(); rand(); rand();
21
22 if (argc != 2)
23 {
24 std::cout << "usage: sap [configfile]" << std::endl;
25 return -1;
26 }
27
28 std::string configfile(argv[1]);
29
30 try
31 {
32 sap bot(configfile, rng);
33
34 try
35 {
36 bot.run();
37 } catch (const std::exception& ex)
38 {
39 std::cout << "Error running bot: " << ex.what() << std::endl;
40 }
41 } catch (const std::exception& ex)
42 {
43 std::cout << "Error initializing bot: " << ex.what() << std::endl;
44 }
45}
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}
diff --git a/sap.h b/sap.h new file mode 100644 index 0000000..5288e57 --- /dev/null +++ b/sap.h
@@ -0,0 +1,34 @@
1#ifndef SAP_H_11D8D668
2#define SAP_H_11D8D668
3
4#include <random>
5#include <string>
6#include <memory>
7#include <Magick++.h>
8#include <twitter.h>
9#include <rawr.h>
10#include "designer.h"
11#include "director.h"
12
13class sap {
14public:
15
16 sap(
17 std::string configFile,
18 std::mt19937& rng);
19
20 void run() const;
21
22private:
23
24 void sendTweet(Magick::Image image) const;
25
26 std::mt19937& rng_;
27 std::unique_ptr<twitter::client> client_;
28 rawr kgramstats_;
29 std::unique_ptr<designer> layout_;
30 std::unique_ptr<director> director_;
31
32};
33
34#endif /* end of include guard: SAP_H_11D8D668 */
diff --git a/util.h b/util.h new file mode 100644 index 0000000..c38f624 --- /dev/null +++ b/util.h
@@ -0,0 +1,60 @@
1#ifndef UTIL_H_1F6A84F6
2#define UTIL_H_1F6A84F6
3
4#include <string>
5#include <sstream>
6#include <iterator>
7
8template <class InputIterator>
9std::string implode(
10 InputIterator first,
11 InputIterator last,
12 std::string delimiter)
13{
14 std::stringstream result;
15
16 for (InputIterator it = first; it != last; it++)
17 {
18 if (it != first)
19 {
20 result << delimiter;
21 }
22
23 result << *it;
24 }
25
26 return result.str();
27}
28
29template <class OutputIterator>
30void split(std::string input, std::string delimiter, OutputIterator out)
31{
32 while (!input.empty())
33 {
34 int divider = input.find(delimiter);
35 if (divider == std::string::npos)
36 {
37 *out = input;
38 out++;
39
40 input = "";
41 } else {
42 *out = input.substr(0, divider);
43 out++;
44
45 input = input.substr(divider+delimiter.length());
46 }
47 }
48}
49
50template <class Container>
51Container split(std::string input, std::string delimiter)
52{
53 Container result;
54
55 split(input, delimiter, std::back_inserter(result));
56
57 return result;
58}
59
60#endif /* end of include guard: UTIL_H_1F6A84F6 */
diff --git a/vendor/rawr-ebooks b/vendor/rawr-ebooks
Subproject ba25493b55b4e4e35de3fca69afd15ddcbaa545 Subproject 247ee4de24eab5ecd030542724db9f69aaa1ed1