@automattic/jetpack-ai-client 0.14.0 → 0.14.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -5,6 +5,11 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.14.1] - 2024-05-27
9
+ ### Changed
10
+ - AI Client: Add paragraph tweaks to Markdown conversion libs. [#37461]
11
+ - AI Featured Image: add type info. [#37474]
12
+
8
13
  ## [0.14.0] - 2024-05-20
9
14
  ### Added
10
15
  - AI Client: Expose HTML render rules type. [#37386]
@@ -323,6 +328,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
323
328
  - Updated package dependencies. [#31659]
324
329
  - Updated package dependencies. [#31785]
325
330
 
331
+ [0.14.1]: https://github.com/Automattic/jetpack-ai-client/compare/v0.14.0...v0.14.1
326
332
  [0.14.0]: https://github.com/Automattic/jetpack-ai-client/compare/v0.13.1...v0.14.0
327
333
  [0.13.1]: https://github.com/Automattic/jetpack-ai-client/compare/v0.13.0...v0.13.1
328
334
  [0.13.0]: https://github.com/Automattic/jetpack-ai-client/compare/v0.12.4...v0.13.0
@@ -1,22 +1,22 @@
1
+ /**
2
+ * The type of the response from the image generation API.
3
+ */
4
+ type ImageGenerationResponse = {
5
+ data: Array<{
6
+ [key: string]: string;
7
+ }>;
8
+ };
1
9
  declare const useImageGenerator: () => {
2
10
  generateImage: ({ feature, postContent, responseFormat, userPrompt, }: {
3
11
  feature: string;
4
12
  postContent: string;
5
13
  responseFormat?: 'url' | 'b64_json';
6
14
  userPrompt?: string;
7
- }) => Promise<{
8
- data: {
9
- [key: string]: string;
10
- }[];
11
- }>;
15
+ }) => Promise<ImageGenerationResponse>;
12
16
  generateImageWithStableDiffusion: ({ feature, postContent, userPrompt, }: {
13
17
  feature: string;
14
18
  postContent: string;
15
19
  userPrompt?: string;
16
- }) => Promise<{
17
- data: {
18
- [key: string]: string;
19
- }[];
20
- }>;
20
+ }) => Promise<ImageGenerationResponse>;
21
21
  };
22
22
  export default useImageGenerator;
@@ -1,7 +1,6 @@
1
1
  /**
2
2
  * External dependencies
3
3
  */
4
- import { __ } from '@wordpress/i18n';
5
4
  import debugFactory from 'debug';
6
5
  /**
7
6
  * Internal dependencies
@@ -130,101 +129,66 @@ const getStableDiffusionImageGenerationPrompt = async (postContent, userPrompt,
130
129
  return data.choices?.[0]?.message?.content;
131
130
  };
132
131
  const useImageGenerator = () => {
133
- const generateImageWithStableDiffusion = async function ({ feature, postContent, userPrompt, }) {
134
- let token = null;
132
+ const executeImageGeneration = async function (parameters) {
133
+ let token = '';
135
134
  try {
136
- token = await requestJwt();
135
+ token = (await requestJwt()).token;
137
136
  }
138
137
  catch (error) {
139
138
  debug('Error getting token: %o', error);
140
139
  return Promise.reject(error);
141
140
  }
142
141
  try {
143
- debug('Generating image with Stable Diffusion');
144
- const prompt = await getStableDiffusionImageGenerationPrompt(postContent, userPrompt, feature);
145
- const data = {
146
- prompt,
147
- style: 'photographic',
148
- token: token.token,
149
- width: 1024,
150
- height: 768,
142
+ const URL = 'https://public-api.wordpress.com/wpcom/v2/jetpack-ai-image';
143
+ const headers = {
144
+ Authorization: `Bearer ${token}`,
145
+ 'Content-Type': 'application/json',
151
146
  };
152
- const response = await fetch(`https://public-api.wordpress.com/wpcom/v2/sites/${token.blogId}/ai-image`, {
147
+ const data = await fetch(URL, {
153
148
  method: 'POST',
154
- headers: {
155
- 'Content-Type': 'application/json',
156
- },
157
- body: JSON.stringify(data),
158
- });
159
- if (!response?.ok) {
160
- debug('Error generating image: %o', response);
161
- return Promise.reject({
162
- data: {
163
- status: response.status,
164
- },
165
- message: __('Error generating image. Please try again later.', 'jetpack-ai-client'),
166
- });
149
+ headers,
150
+ body: JSON.stringify(parameters),
151
+ }).then(response => response.json());
152
+ if (data?.data?.status && data?.data?.status > 200) {
153
+ debug('Error generating image: %o', data);
154
+ return Promise.reject(data);
167
155
  }
168
- const blob = await response.blob();
169
- /**
170
- * Convert the blob to base64 to keep the same format as the Dalle API.
171
- */
172
- const base64 = await new Promise((resolve, reject) => {
173
- const reader = new FileReader();
174
- reader.onloadend = () => {
175
- const base64data = reader.result;
176
- return resolve(base64data.replace(/^data:image\/(png|jpg);base64,/, ''));
177
- };
178
- reader.onerror = reject;
179
- reader.readAsDataURL(blob);
180
- });
181
- // Return the Dalle API format
182
- return {
183
- data: [
184
- {
185
- b64_json: base64,
186
- revised_prompt: prompt,
187
- },
188
- ],
189
- };
156
+ return data;
190
157
  }
191
158
  catch (error) {
192
159
  debug('Error generating image: %o', error);
193
160
  return Promise.reject(error);
194
161
  }
195
162
  };
196
- const generateImage = async function ({ feature, postContent, responseFormat = 'url', userPrompt, }) {
197
- let token = '';
163
+ const generateImageWithStableDiffusion = async function ({ feature, postContent, userPrompt, }) {
198
164
  try {
199
- token = (await requestJwt()).token;
165
+ debug('Generating image with Stable Diffusion');
166
+ const prompt = await getStableDiffusionImageGenerationPrompt(postContent, userPrompt, feature);
167
+ const parameters = {
168
+ prompt,
169
+ feature,
170
+ model: 'stable-diffusion',
171
+ style: 'photographic',
172
+ };
173
+ const data = await executeImageGeneration(parameters);
174
+ return data;
200
175
  }
201
176
  catch (error) {
202
- debug('Error getting token: %o', error);
177
+ debug('Error generating image: %o', error);
203
178
  return Promise.reject(error);
204
179
  }
180
+ };
181
+ const generateImage = async function ({ feature, postContent, responseFormat = 'url', userPrompt, }) {
205
182
  try {
206
183
  debug('Generating image');
207
184
  const imageGenerationPrompt = getDalleImageGenerationPrompt(postContent, userPrompt);
208
- const URL = 'https://public-api.wordpress.com/wpcom/v2/jetpack-ai-image';
209
- const body = {
185
+ const parameters = {
210
186
  prompt: imageGenerationPrompt,
211
187
  response_format: responseFormat,
212
188
  feature,
213
189
  size: '1792x1024',
214
190
  };
215
- const headers = {
216
- Authorization: `Bearer ${token}`,
217
- 'Content-Type': 'application/json',
218
- };
219
- const data = await fetch(URL, {
220
- method: 'POST',
221
- headers,
222
- body: JSON.stringify(body),
223
- }).then(response => response.json());
224
- if (data?.data?.status && data?.data?.status > 200) {
225
- debug('Error generating image: %o', data);
226
- return Promise.reject(data);
227
- }
191
+ const data = await executeImageGeneration(parameters);
228
192
  return data;
229
193
  }
230
194
  catch (error) {
@@ -5,11 +5,19 @@ import TurndownService from 'turndown';
5
5
  /**
6
6
  * Types
7
7
  */
8
- import type { Options, Rule } from 'turndown';
8
+ import type { Options, Rule, Filter } from 'turndown';
9
+ export type Fix = 'paragraph';
9
10
  export default class HTMLToMarkdown {
10
11
  turndownService: TurndownService;
11
- constructor(options?: Options, rules?: {
12
- [key: string]: Rule;
12
+ fixes: Fix[];
13
+ constructor({ options, rules, keep, remove, fixes, }?: {
14
+ options?: Options;
15
+ rules?: {
16
+ [key: string]: Rule;
17
+ };
18
+ keep?: Filter;
19
+ remove?: Filter;
20
+ fixes?: Fix[];
13
21
  });
14
22
  /**
15
23
  * Renders HTML from Markdown content with specified processing rules.
@@ -2,6 +2,12 @@
2
2
  * External dependencies
3
3
  */
4
4
  import TurndownService from 'turndown';
5
+ const fixesList = {
6
+ paragraph: (content) => {
7
+ // Keep <br> tags to prevent paragraphs from being split
8
+ return content.replaceAll('\n', '<br />');
9
+ },
10
+ };
5
11
  const defaultTurndownOptions = { emDelimiter: '_', headingStyle: 'atx' };
6
12
  const defaultTurndownRules = {
7
13
  strikethrough: {
@@ -13,10 +19,15 @@ const defaultTurndownRules = {
13
19
  };
14
20
  export default class HTMLToMarkdown {
15
21
  turndownService;
16
- constructor(options = defaultTurndownOptions, rules = defaultTurndownRules) {
17
- this.turndownService = new TurndownService(options);
18
- for (const rule in rules) {
19
- this.turndownService.addRule(rule, rules[rule]);
22
+ fixes;
23
+ constructor({ options = {}, rules = {}, keep = [], remove = [], fixes = [], } = {}) {
24
+ this.fixes = fixes;
25
+ this.turndownService = new TurndownService({ ...defaultTurndownOptions, ...options });
26
+ this.turndownService.keep(keep);
27
+ this.turndownService.remove(remove);
28
+ const allRules = { ...defaultTurndownRules, ...rules };
29
+ for (const rule in allRules) {
30
+ this.turndownService.addRule(rule, allRules[rule]);
20
31
  }
21
32
  }
22
33
  /**
@@ -26,6 +37,9 @@ export default class HTMLToMarkdown {
26
37
  * @returns {string} The rendered Markdown content
27
38
  */
28
39
  render({ content }) {
29
- return this.turndownService.turndown(content);
40
+ const rendered = this.turndownService.turndown(content);
41
+ return this.fixes.reduce((renderedContent, fix) => {
42
+ return fixesList[fix](renderedContent);
43
+ }, rendered);
30
44
  }
31
45
  }
@@ -6,7 +6,7 @@ import MarkdownIt from 'markdown-it';
6
6
  * Types
7
7
  */
8
8
  import type { Options } from 'markdown-it';
9
- export type Fix = 'list';
9
+ export type Fix = 'list' | 'paragraph';
10
10
  export default class MarkdownToHTML {
11
11
  markdownConverter: MarkdownIt;
12
12
  constructor(options?: Options);
@@ -7,6 +7,10 @@ const fixes = {
7
7
  // Fix list indentation
8
8
  return content.replace(/<li>\s+<p>/g, '<li>').replace(/<\/p>\s+<\/li>/g, '</li>');
9
9
  },
10
+ paragraph: (content) => {
11
+ // Fix encoding of <br /> tags
12
+ return content.replaceAll(/\s*&lt;br \/&gt;\s*/g, '<br />');
13
+ },
10
14
  };
11
15
  const defaultMarkdownItOptions = {
12
16
  breaks: true,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "private": false,
3
3
  "name": "@automattic/jetpack-ai-client",
4
- "version": "0.14.0",
4
+ "version": "0.14.1",
5
5
  "description": "A JS client for consuming Jetpack AI services",
6
6
  "homepage": "https://github.com/Automattic/jetpack/tree/HEAD/projects/js-packages/ai-client/#readme",
7
7
  "bugs": {
@@ -1,7 +1,6 @@
1
1
  /**
2
2
  * External dependencies
3
3
  */
4
- import { __ } from '@wordpress/i18n';
5
4
  import debugFactory from 'debug';
6
5
  /**
7
6
  * Internal dependencies
@@ -11,6 +10,13 @@ import requestJwt from '../../jwt/index.js';
11
10
 
12
11
  const debug = debugFactory( 'ai-client:use-image-generator' );
13
12
 
13
+ /**
14
+ * The type of the response from the image generation API.
15
+ */
16
+ type ImageGenerationResponse = {
17
+ data: Array< { [ key: string ]: string } >;
18
+ };
19
+
14
20
  /**
15
21
  * Cut the post content on a given lenght so the total length of the prompt is not longer than 4000 characters.
16
22
  * @param {string} content - the content to be truncated
@@ -148,6 +154,44 @@ const getStableDiffusionImageGenerationPrompt = async (
148
154
  };
149
155
 
150
156
  const useImageGenerator = () => {
157
+ const executeImageGeneration = async function ( parameters: {
158
+ [ key: string ]: string;
159
+ } ): Promise< ImageGenerationResponse > {
160
+ let token = '';
161
+
162
+ try {
163
+ token = ( await requestJwt() ).token;
164
+ } catch ( error ) {
165
+ debug( 'Error getting token: %o', error );
166
+ return Promise.reject( error );
167
+ }
168
+
169
+ try {
170
+ const URL = 'https://public-api.wordpress.com/wpcom/v2/jetpack-ai-image';
171
+
172
+ const headers = {
173
+ Authorization: `Bearer ${ token }`,
174
+ 'Content-Type': 'application/json',
175
+ };
176
+
177
+ const data = await fetch( URL, {
178
+ method: 'POST',
179
+ headers,
180
+ body: JSON.stringify( parameters ),
181
+ } ).then( response => response.json() );
182
+
183
+ if ( data?.data?.status && data?.data?.status > 200 ) {
184
+ debug( 'Error generating image: %o', data );
185
+ return Promise.reject( data );
186
+ }
187
+
188
+ return data as ImageGenerationResponse;
189
+ } catch ( error ) {
190
+ debug( 'Error generating image: %o', error );
191
+ return Promise.reject( error );
192
+ }
193
+ };
194
+
151
195
  const generateImageWithStableDiffusion = async function ( {
152
196
  feature,
153
197
  postContent,
@@ -156,16 +200,7 @@ const useImageGenerator = () => {
156
200
  feature: string;
157
201
  postContent: string;
158
202
  userPrompt?: string;
159
- } ): Promise< { data: Array< { [ key: string ]: string } > } > {
160
- let token = null;
161
-
162
- try {
163
- token = await requestJwt();
164
- } catch ( error ) {
165
- debug( 'Error getting token: %o', error );
166
- return Promise.reject( error );
167
- }
168
-
203
+ } ): Promise< ImageGenerationResponse > {
169
204
  try {
170
205
  debug( 'Generating image with Stable Diffusion' );
171
206
 
@@ -175,59 +210,15 @@ const useImageGenerator = () => {
175
210
  feature
176
211
  );
177
212
 
178
- const data = {
213
+ const parameters = {
179
214
  prompt,
215
+ feature,
216
+ model: 'stable-diffusion',
180
217
  style: 'photographic',
181
- token: token.token,
182
- width: 1024,
183
- height: 768,
184
218
  };
185
219
 
186
- const response = await fetch(
187
- `https://public-api.wordpress.com/wpcom/v2/sites/${ token.blogId }/ai-image`,
188
- {
189
- method: 'POST',
190
- headers: {
191
- 'Content-Type': 'application/json',
192
- },
193
- body: JSON.stringify( data ),
194
- }
195
- );
196
-
197
- if ( ! response?.ok ) {
198
- debug( 'Error generating image: %o', response );
199
- return Promise.reject( {
200
- data: {
201
- status: response.status,
202
- },
203
- message: __( 'Error generating image. Please try again later.', 'jetpack-ai-client' ),
204
- } );
205
- }
206
-
207
- const blob = await response.blob();
208
-
209
- /**
210
- * Convert the blob to base64 to keep the same format as the Dalle API.
211
- */
212
- const base64 = await new Promise( ( resolve, reject ) => {
213
- const reader = new FileReader();
214
- reader.onloadend = () => {
215
- const base64data = reader.result as string;
216
- return resolve( base64data.replace( /^data:image\/(png|jpg);base64,/, '' ) );
217
- };
218
- reader.onerror = reject;
219
- reader.readAsDataURL( blob );
220
- } );
221
-
222
- // Return the Dalle API format
223
- return {
224
- data: [
225
- {
226
- b64_json: base64 as string,
227
- revised_prompt: prompt,
228
- },
229
- ],
230
- };
220
+ const data: ImageGenerationResponse = await executeImageGeneration( parameters );
221
+ return data;
231
222
  } catch ( error ) {
232
223
  debug( 'Error generating image: %o', error );
233
224
  return Promise.reject( error );
@@ -244,47 +235,21 @@ const useImageGenerator = () => {
244
235
  postContent: string;
245
236
  responseFormat?: 'url' | 'b64_json';
246
237
  userPrompt?: string;
247
- } ): Promise< { data: Array< { [ key: string ]: string } > } > {
248
- let token = '';
249
-
250
- try {
251
- token = ( await requestJwt() ).token;
252
- } catch ( error ) {
253
- debug( 'Error getting token: %o', error );
254
- return Promise.reject( error );
255
- }
256
-
238
+ } ): Promise< ImageGenerationResponse > {
257
239
  try {
258
240
  debug( 'Generating image' );
259
241
 
260
242
  const imageGenerationPrompt = getDalleImageGenerationPrompt( postContent, userPrompt );
261
243
 
262
- const URL = 'https://public-api.wordpress.com/wpcom/v2/jetpack-ai-image';
263
-
264
- const body = {
244
+ const parameters = {
265
245
  prompt: imageGenerationPrompt,
266
246
  response_format: responseFormat,
267
247
  feature,
268
248
  size: '1792x1024',
269
249
  };
270
250
 
271
- const headers = {
272
- Authorization: `Bearer ${ token }`,
273
- 'Content-Type': 'application/json',
274
- };
275
-
276
- const data = await fetch( URL, {
277
- method: 'POST',
278
- headers,
279
- body: JSON.stringify( body ),
280
- } ).then( response => response.json() );
281
-
282
- if ( data?.data?.status && data?.data?.status > 200 ) {
283
- debug( 'Error generating image: %o', data );
284
- return Promise.reject( data );
285
- }
286
-
287
- return data as { data: { [ key: string ]: string }[] };
251
+ const data: ImageGenerationResponse = await executeImageGeneration( parameters );
252
+ return data;
288
253
  } catch ( error ) {
289
254
  debug( 'Error generating image: %o', error );
290
255
  return Promise.reject( error );
@@ -35,7 +35,7 @@ const rules = {
35
35
  }
36
36
  }
37
37
  };
38
- const renderer = new HTMLToMarkdown( options, rules );
38
+ const renderer = new HTMLToMarkdown( { options, rules } );
39
39
  const markdownContent = renderer.render( { content: htmlContent } );
40
40
  // ***Hello world***
41
41
  ```
@@ -5,7 +5,19 @@ import TurndownService from 'turndown';
5
5
  /**
6
6
  * Types
7
7
  */
8
- import type { Options, Rule } from 'turndown';
8
+ import type { Options, Rule, Filter } from 'turndown';
9
+
10
+ export type Fix = 'paragraph';
11
+ type Fixes = {
12
+ [ key in Fix ]: ( content: string ) => string;
13
+ };
14
+
15
+ const fixesList: Fixes = {
16
+ paragraph: ( content: string ) => {
17
+ // Keep <br> tags to prevent paragraphs from being split
18
+ return content.replaceAll( '\n', '<br />' );
19
+ },
20
+ };
9
21
 
10
22
  const defaultTurndownOptions: Options = { emDelimiter: '_', headingStyle: 'atx' };
11
23
  const defaultTurndownRules: { [ key: string ]: Rule } = {
@@ -19,14 +31,29 @@ const defaultTurndownRules: { [ key: string ]: Rule } = {
19
31
 
20
32
  export default class HTMLToMarkdown {
21
33
  turndownService: TurndownService;
34
+ fixes: Fix[];
35
+
36
+ constructor( {
37
+ options = {},
38
+ rules = {},
39
+ keep = [],
40
+ remove = [],
41
+ fixes = [],
42
+ }: {
43
+ options?: Options;
44
+ rules?: { [ key: string ]: Rule };
45
+ keep?: Filter;
46
+ remove?: Filter;
47
+ fixes?: Fix[];
48
+ } = {} ) {
49
+ this.fixes = fixes;
50
+ this.turndownService = new TurndownService( { ...defaultTurndownOptions, ...options } );
51
+ this.turndownService.keep( keep );
52
+ this.turndownService.remove( remove );
22
53
 
23
- constructor(
24
- options: Options = defaultTurndownOptions,
25
- rules: { [ key: string ]: Rule } = defaultTurndownRules
26
- ) {
27
- this.turndownService = new TurndownService( options );
28
- for ( const rule in rules ) {
29
- this.turndownService.addRule( rule, rules[ rule ] );
54
+ const allRules = { ...defaultTurndownRules, ...rules };
55
+ for ( const rule in allRules ) {
56
+ this.turndownService.addRule( rule, allRules[ rule ] );
30
57
  }
31
58
  }
32
59
 
@@ -37,6 +64,10 @@ export default class HTMLToMarkdown {
37
64
  * @returns {string} The rendered Markdown content
38
65
  */
39
66
  render( { content }: { content: string } ): string {
40
- return this.turndownService.turndown( content );
67
+ const rendered = this.turndownService.turndown( content );
68
+
69
+ return this.fixes.reduce( ( renderedContent, fix ) => {
70
+ return fixesList[ fix ]( renderedContent );
71
+ }, rendered );
41
72
  }
42
73
  }
@@ -7,7 +7,7 @@ import MarkdownIt from 'markdown-it';
7
7
  */
8
8
  import type { Options } from 'markdown-it';
9
9
 
10
- export type Fix = 'list';
10
+ export type Fix = 'list' | 'paragraph';
11
11
  type Fixes = {
12
12
  [ key in Fix ]: ( content: string ) => string;
13
13
  };
@@ -17,6 +17,10 @@ const fixes: Fixes = {
17
17
  // Fix list indentation
18
18
  return content.replace( /<li>\s+<p>/g, '<li>' ).replace( /<\/p>\s+<\/li>/g, '</li>' );
19
19
  },
20
+ paragraph: ( content: string ) => {
21
+ // Fix encoding of <br /> tags
22
+ return content.replaceAll( /\s*&lt;br \/&gt;\s*/g, '<br />' );
23
+ },
20
24
  };
21
25
 
22
26
  const defaultMarkdownItOptions: Options = {