PluginProbe
BeyondWords – AI audio for publishers / 4.1.1
BeyondWords – AI audio for publishers v4.1.1
7.1.0 trunk 4.0.0 4.0.1 4.0.2 4.0.3 4.0.4 4.0.5 4.0.6 4.1.0 4.1.1 4.1.2 4.2.0 4.2.1 4.2.2 4.2.3 4.2.4 4.3.0 4.4.0 4.5.0 4.5.1 4.6.0 4.6.1 4.6.2 4.7.0 All 43 releases
speechkit / src / Component / Post / PostContentUtils.php

PostContentUtils.php in BeyondWords – AI audio for publishers 4.1.1, at src/Component/Post/PostContentUtils.php

465 lines 14.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 declare(strict_types=1);
4
5 namespace Beyondwords\Wordpress\Component\Post;
6
7 /**
8 * BeyondWords Post Content Utilities.
9 *
10 * @package Beyondwords
11 * @subpackage Beyondwords/includes
12 * @author Stuart McAlpine <stu@beyondwords.io>
13 * @since 3.5.0
14 */
15 class PostContentUtils
16 {
17 public const DATE_FORMAT = 'Y-m-d\TH:i:s\Z';
18
19 /**
20 * Get the source text for the audio, ready to be sent to the BeyondWords API.
21 *
22 * @deprecated 4.0.0 Renamed to PostContentUtils::getBody()
23 *
24 * @param int|WP_Post $post The WordPress post ID, or post object.
25 *
26 * @since 3.0.0
27 * @since 3.5.0 Moved from Core\Utils to Component\Post\PostUtils
28 * @since 3.8.0 Exclude Gutenberg blocks with attribute { beyondwordsAudio: false }
29 * @since 4.0.0 Renamed from PostContentUtils::getSourceTextForAudio() to PostContentUtils::getBody()
30 *
31 * @return string The body (the processed $post->post_content).
32 */
33 public static function getSourceTextForAudio($post)
34 {
35 _doing_it_wrong(
36 'PostContentUtils::getBody',
37 'BeyondWords PostContentUtils::getSourceTextForAudio() has been renamed to PostContentUtils::getBody()',
38 '4.0.0'
39 );
40
41 return PostContentUtils::getBody($post);
42 }
43
44 /**
45 * Get the body for the audio, ready to be sent to the BeyondWords API.
46 *
47 * The following rules are applied:
48 *
49 * Main body content entered in WordPress
50 * + Optionally filtered using [SpeechKit-Start]/[SpeechKit-Stop] "shortcodes"
51 * + With registered content filters FROM OTHER PLUGINS applied
52 * + Optionally prepended with the Post excerpt
53 * + Optionally filtered using the beyondwords_content filter
54 *
55 * @SuppressWarnings(PHPMD.LongVariable)
56 *
57 * @param int|WP_Post $post The WordPress post ID, or post object.
58 *
59 * @since 3.0.0
60 * @since 3.5.0 Moved from Core\Utils to Component\Post\PostUtils
61 * @since 3.8.0 Exclude Gutenberg blocks with attribute { beyondwordsAudio: false }
62 * @since 4.0.0 Renamed from PostContentUtils::getSourceTextForAudio() to PostContentUtils::getBody()
63 *
64 * @return string The body (the processed $post->post_content).
65 */
66 public static function getBody($post)
67 {
68 global $beyondwords_wordpress_plugin;
69
70 $post = get_post($post);
71
72 if (!($post instanceof \WP_Post)) {
73 throw new \Exception('Post Not Found');
74 }
75
76 $content = PostContentUtils::getContentWithoutExcludedBlocks($post);
77
78 // If SpeechKit-Start/Stop tags are present then use the content within them
79 // @deprecated v3.0.0: publishers should use the beyondwords_content filter instead.
80 $regex = '/\[SpeechKit-Start\](.*?)\[SpeechKit-Stop\]/s';
81
82 if (preg_match_all($regex, $content, $match, PREG_PATTERN_ORDER) > 0) {
83 $content = implode(' ', $match[1]);
84 }
85
86 // Temporarily remove our Player filter, to exclude the player <div>
87 if ($beyondwords_wordpress_plugin && isset($beyondwords_wordpress_plugin->player)) {
88 remove_filter('the_content', array($beyondwords_wordpress_plugin->player, 'addPlayerToContent'));
89 }
90
91 // Apply other standard WordPress filters to handle shortcodes etc
92 $content = apply_filters('the_content', $content);
93
94 // Add our Player filter back in again
95 if ($beyondwords_wordpress_plugin && isset($beyondwords_wordpress_plugin->player)) {
96 add_filter('the_content', array($beyondwords_wordpress_plugin->player, 'addPlayerToContent'));
97 }
98
99 // TODO maybe we need to do something like this to deal with Shortcodes a little better?
100 // $content = wp_strip_all_tags(apply_filters('the_content', $content));
101
102 // Trim to remove trailing newlines – common for WordPress content
103 $content = trim($content);
104
105 /**
106 * Filters the content body we send for audio processing.
107 *
108 * @since 4.0.0
109 *
110 * @param string $content The post content.
111 * @param int $postId The post ID.
112 */
113 $content = apply_filters('beyondwords_content', $content, $post->ID);
114
115 return $content;
116 }
117
118 /**
119 * Get the summary for the audio content, ready to be sent to the BeyondWords API.
120 *
121 * @param int|WP_Post $post The WordPress post ID, or post object.
122 *
123 * @since 4.0.0
124 *
125 * @return string The summary.
126 */
127 public static function getSummary($post)
128 {
129 $post = get_post($post);
130
131 if (!($post instanceof \WP_Post)) {
132 throw new \Exception('Post Not Found');
133 }
134
135 $summary = null;
136
137 // Optionally send the excerpt to the REST API, if the plugin setting has been checked
138 $prependExcerpt = get_option('beyondwords_prepend_excerpt');
139
140 if ($prependExcerpt && has_excerpt($post)) {
141 // Escape characters
142 $summary = htmlentities($post->post_excerpt, ENT_QUOTES | ENT_XHTML);
143 // Apply WordPress filters
144 $summary = apply_filters('get_the_excerpt', $summary);
145 // Convert line breaks into paragraphs
146 $summary = trim(wpautop($summary));
147 }
148
149 return $summary;
150 }
151
152 /**
153 * Get the segments for the audio content, ready to be sent to the BeyondWords API.
154 *
155 * THIS METHOD IS CURRENTLY NOT IN USE. Segments cannot currently include HTML
156 * formatting tags such as <strong> and <em> so we do not pass segments, we pass
157 * a HTML string as the body param instead.
158 *
159 * @param int|WP_Post $post The WordPress post ID, or post object.
160 *
161 * @since 4.0.0
162 *
163 * @return array|null The segments.
164 */
165 public static function getSegments($post)
166 {
167 if (! has_blocks($post)) {
168 return null;
169 }
170
171 $titleSegment = (object) [
172 'section' => 'title',
173 'text' => get_the_title($post),
174 ];
175
176 $summarySegment = (object) [
177 'section' => 'summary',
178 'text' => PostContentUtils::getSummary($post),
179 ];
180
181 $blocks = PostContentUtils::getAudioEnabledBlocks($post);
182
183 $bodySegments = array_map(function ($block) {
184 $marker = null;
185
186 if (isset($block['attrs']) && isset($block['attrs']['beyondwordsMarker'])) {
187 $marker = $block['attrs']['beyondwordsMarker'];
188 }
189
190 return (object) [
191 'section' => 'body',
192 'marker' => $marker,
193 'text' => trim(render_block($block)),
194 ];
195 }, $blocks);
196
197 // Merge title, summary and body segments
198 $segments = array_values(array_merge([$titleSegment], [$summarySegment], $bodySegments));
199
200 // TODO Consider removing this when API can handle it
201 // Remove any segments with empty text
202 $segments = array_values(array_filter($segments, function ($segment) {
203 return (! empty($segment->text));
204 }));
205
206 return $segments;
207 }
208
209 /**
210 * Get the post content without blocks which have been filtered.
211 *
212 * We have added buttons into the Gutenberg editor to optionally exclude selected
213 * blocks from the source text for audio.
214 *
215 * This method filters all blocks, removing any which have been excluded.
216 *
217 * @param int|WP_Post $post The WordPress post ID, or post object.
218 *
219 * @since 3.8.0
220 * @since 4.0.0 Replace for loop with array_reduce
221 *
222 * @return string The post body without excluded blocks.
223 */
224 public static function getContentWithoutExcludedBlocks($post)
225 {
226 if (! has_blocks($post)) {
227 return trim($post->post_content);
228 }
229
230 $blocks = parse_blocks($post->post_content);
231
232 if (! count($blocks)) {
233 return trim($post->post_content);
234 }
235
236 $blocks = PostContentUtils::getAudioEnabledBlocks($post);
237
238 return array_reduce($blocks, static function ($carry, $block) {
239 return $carry . trim(render_block($block));
240 }, '');
241 }
242
243 /**
244 * Get audio-enabled blocks.
245 *
246 * @param int|WP_Post $post The WordPress post ID, or post object.
247 *
248 * @since 4.0.0
249 *
250 * @return array The blocks.
251 */
252 public static function getAudioEnabledBlocks($post)
253 {
254 $post = get_post($post);
255
256 if (! ($post instanceof \WP_Post)) {
257 return [];
258 }
259
260 if (! has_blocks($post)) {
261 return [];
262 }
263
264 $allBlocks = parse_blocks($post->post_content);
265
266 $blocks = array_filter($allBlocks, function ($block) {
267 $enabled = true;
268
269 if (is_array($block['attrs']) && isset($block['attrs']['beyondwordsAudio'])) {
270 $enabled = (bool) $block['attrs']['beyondwordsAudio'];
271 }
272
273 return $enabled;
274 });
275
276 /**
277 * Filters the audio-enabled blocks for a post.
278 *
279 * @since 4.0.0
280 *
281 * @param array $blocks The audio-enabled post blocks.
282 * @param array $allBlocks All post blocks including those with audio disabled.
283 * @param int $postId The post ID.
284 */
285 $blocks = apply_filters('beyondwords_post_audio_enabled_blocks', $blocks, $allBlocks, $post->ID);
286
287 return $blocks;
288 }
289
290 /**
291 * Get the body param we pass to the API.
292 *
293 * @since 3.0.0
294 * @since 3.3.0 Added metadata to aid custom playlist generation.
295 * @since 3.5.0 Moved from Core\Utils to Component\Post\PostUtils.
296 * @since 3.10.4 Rename `published_at` API param to `publish_date`.
297 * @since 4.0.0 Use new API params.
298 * @since 4.0.3 Ensure `image_url` is always a string.
299 *
300 * @static
301 * @param int $postId WordPress Post ID.
302 *
303 * @return Response
304 **/
305 public static function getBodyJson($postId)
306 {
307 // https://beyondwords.postman.co/workspace/BeyondWords-Developers~169d849b-f6e1-4a78-8b66-5c87969588fc/request/23751538-99727151-c025-43b3-bd12-66951a042e2c
308 $body = [
309 'type' => 'auto_segment',
310 'title' => get_the_title($postId),
311 'summary' => PostContentUtils::getSummary($postId),
312 'body' => PostContentUtils::getBody($postId),
313 'source_url' => get_the_permalink($postId),
314 'source_id' => strval($postId),
315 'author' => PostContentUtils::getAuthorName($postId),
316 'image_url' => strval(get_the_post_thumbnail_url($postId, [240, 240])),
317 'metadata' => PostContentUtils::getMetadata($postId),
318 'published' => true,
319 'publish_date' => get_post_time(PostContentUtils::DATE_FORMAT, true, $postId),
320 ];
321
322 $status = get_post_status($postId);
323
324 /*
325 * If the post status is "pending" then we send { published: false } to
326 * the BeyondWords API, to prevent the generated audio from being
327 * published in playlists.
328 *
329 * We also omit { publish_date } because get_post_time() returns `false`
330 * for posts which are "Pending Review".
331 */
332 if ($status === 'pending') {
333 $body['published'] = false;
334 unset($body['publish_date']);
335 }
336
337 $bodyVoiceId = intval(get_post_meta($postId, 'beyondwords_body_voice_id', true));
338
339 if ($bodyVoiceId > 0) {
340 $body['body_voice_id'] = $bodyVoiceId;
341 }
342
343 $titleVoiceId = intval(get_post_meta($postId, 'beyondwords_title_voice_id', true));
344
345 if ($titleVoiceId > 0) {
346 $body['title_voice_id'] = $titleVoiceId;
347 }
348
349 $summaryVoiceId = intval(get_post_meta($postId, 'beyondwords_summary_voice_id', true));
350
351 if ($summaryVoiceId > 0) {
352 $body['summary_voice_id'] = $summaryVoiceId;
353 }
354
355 /**
356 * Filters the body params we send to the BeyondWords API when
357 * processing audio.
358 *
359 * @since 4.0.0
360 *
361 * @param array $body The params we send to the BeyondWords API.
362 * @param array $postId WordPress post ID.
363 */
364 $body = apply_filters('beyondwords_body_params', $body, $postId);
365
366 return wp_json_encode($body);
367 }
368
369 /**
370 * Get the post metadata to send with BeyondWords API requests.
371 *
372 * The metadata key is defined by the BeyondWords API as "A custom object
373 * for storing meta information".
374 *
375 * The metadata values are used to create filters for playlists in the
376 * BeyondWords dashboard.
377 *
378 * We currently only include taxonomies by default, and the output of this
379 * method can be filtered using the `beyondwords_post_metadata` filter.
380 *
381 * @since 3.3.0
382 * @since 3.5.0 Moved from Core\Utils to Component\Post\PostUtils
383 *
384 * @param int $postId Post ID.
385 *
386 * @return array
387 */
388 public static function getMetadata($postId)
389 {
390 $metadata = new \stdClass();
391
392 $taxonomy = PostContentUtils::getAllTaxonomiesAndTerms($postId);
393
394 if (count((array)$taxonomy)) {
395 $metadata->taxonomy = $taxonomy;
396 }
397
398 /**
399 * Filters the post metadata sent to the BeyondWords API.
400 *
401 * @since 3.3.0
402 *
403 * @param object $metadata Post metadata. Defaults to the taxonomies and terms assigned to the post.
404 * @param int $postId Post ID.
405 */
406 $metadata = apply_filters('beyondwords_post_metadata', $metadata, $postId);
407
408 return $metadata;
409 }
410
411 /**
412 * Get all taxonomies, and their selected terms, for a post.
413 *
414 * Returns an associative array of taxonomy names and terms.
415 *
416 * For example:
417 *
418 * array(
419 * "categories" => array("Category 1"),
420 * "post_tag" => array("Tag 1", "Tag 2", "Tag 3"),
421 * )
422 *
423 * @since 3.3.0
424 * @since 3.5.0 Moved from Core\Utils to Component\Post\PostUtils
425 *
426 * @param int $postId Post ID.
427 *
428 * @return array
429 */
430 public static function getAllTaxonomiesAndTerms($postId)
431 {
432 $postType = get_post_type($postId);
433
434 $postTypeTaxonomies = get_object_taxonomies($postType);
435
436 $taxonomies = new \stdClass();
437
438 foreach ($postTypeTaxonomies as $postTypeTaxonomy) {
439 $terms = get_the_terms($postId, $postTypeTaxonomy);
440
441 if (! empty($terms) && ! is_wp_error($terms)) {
442 $taxonomies->{(string)$postTypeTaxonomy} = wp_list_pluck($terms, 'name');
443 }
444 }
445
446 return $taxonomies;
447 }
448
449 /**
450 * Get author name for a post.
451 *
452 * @since 3.10.4
453 *
454 * @param int $postId Post ID.
455 *
456 * @return string
457 */
458 public static function getAuthorName($postId)
459 {
460 $authorId = get_post_field('post_author', $postId);
461
462 return get_the_author_meta('display_name', $authorId);
463 }
464 }
465