From f51578f9b0e858e3898bfa0f420dbf63fd7ac585 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=97=8D+85CD?= <50108258+kwaa@users.noreply.github.com> Date: Thu, 27 Mar 2025 20:22:13 +0800 Subject: [PATCH] feat: new package `generate-image` (#123) * feat: new package `generate-image` * fix(generate-image): url path * fix: lint errors * docs: add generate-image * chore(xsai): update exports --------- Co-authored-by: Neko --- cspell.config.yaml | 3 + docs/content/docs/packages/generate/image.mdx | 34 ++++++++ docs/content/docs/packages/generate/meta.json | 3 +- docs/content/docs/references/meta.json | 1 + docs/package.json | 1 + eslint.config.ts | 61 ++++++-------- packages-top/xsai/package.json | 1 + packages-top/xsai/src/index.ts | 1 + packages/generate-image/README.md | 1 + packages/generate-image/package.json | 42 ++++++++++ packages/generate-image/src/index.ts | 79 +++++++++++++++++++ packages/generate-object/src/index.ts | 1 + packages/stream-object/src/index.ts | 4 +- .../utils-stream/src/to-async-iterator.ts | 1 + pnpm-lock.yaml | 12 +++ pnpm-workspace.yaml | 7 +- 16 files changed, 208 insertions(+), 44 deletions(-) create mode 100644 docs/content/docs/packages/generate/image.mdx create mode 100644 packages/generate-image/README.md create mode 100644 packages/generate-image/package.json create mode 100644 packages/generate-image/src/index.ts diff --git a/cspell.config.yaml b/cspell.config.yaml index 1b8c3ad..2bdab4e 100644 --- a/cspell.config.yaml +++ b/cspell.config.yaml @@ -3,17 +3,20 @@ ignorePaths: [] dictionaryDefinitions: [] dictionaries: [] words: + - AAAAIGZ - agentic - all-minilm - arktype - automd - BAAI + - Bhdmlm - bumpp - Cerebras - changelogithub - codestral - cognitivecomputations - composio + - dall - deepinfra - deepseek - elevenlabs diff --git a/docs/content/docs/packages/generate/image.mdx b/docs/content/docs/packages/generate/image.mdx new file mode 100644 index 0000000..a33beb2 --- /dev/null +++ b/docs/content/docs/packages/generate/image.mdx @@ -0,0 +1,34 @@ +--- +title: Image +description: Creates an image given a prompt. +--- + +import { PkgSizeBadge } from '@/components/pkg-size-badge' + + + +```package-install +npm i @xsai/generate-image +``` + +## Examples + +```ts twoslash +import { generateImage } from '@xsai/generate-image' +import { env } from 'node:process' + +const { image } = await generateImage({ + apiKey: env.OPENAI_API_KEY!, + baseURL: 'http://api.openai.com/v1/', + model: 'dall-e-3', + prompt: 'A cute baby sea otter' +}) + +const { images } = await generateImage({ + apiKey: env.OPENAI_API_KEY!, + baseURL: 'http://api.openai.com/v1/', + n: 4, // [!code highlight] + model: 'dall-e-3', + prompt: 'A cute baby sea otter' +}) +``` diff --git a/docs/content/docs/packages/generate/meta.json b/docs/content/docs/packages/generate/meta.json index 2315ef0..815deb6 100644 --- a/docs/content/docs/packages/generate/meta.json +++ b/docs/content/docs/packages/generate/meta.json @@ -5,6 +5,7 @@ "text", "object", "speech", - "transcription" + "transcription", + "image" ] } diff --git a/docs/content/docs/references/meta.json b/docs/content/docs/references/meta.json index 22a91bf..6f50f93 100644 --- a/docs/content/docs/references/meta.json +++ b/docs/content/docs/references/meta.json @@ -2,6 +2,7 @@ "defaultOpen": false, "pages": [ "[@xsai/embed](https://tsdocs.dev/search/docs/@xsai/embed)", + "[@xsai/generate-image](https://tsdocs.dev/search/docs/@xsai/generate-image)", "[@xsai/generate-object](https://tsdocs.dev/search/docs/@xsai/generate-object)", "[@xsai/generate-speech](https://tsdocs.dev/search/docs/@xsai/generate-speech)", "[@xsai/generate-text](https://tsdocs.dev/search/docs/@xsai/generate-text)", diff --git a/docs/package.json b/docs/package.json index 247a561..23c35a5 100644 --- a/docs/package.json +++ b/docs/package.json @@ -35,6 +35,7 @@ "@xsai-ext/providers-cloud": "workspace:^", "@xsai-ext/providers-local": "workspace:^", "@xsai/embed": "workspace:^", + "@xsai/generate-image": "workspace:^", "@xsai/generate-object": "workspace:^", "@xsai/generate-speech": "workspace:^", "@xsai/generate-text": "workspace:^", diff --git a/eslint.config.ts b/eslint.config.ts index 8b6dda3..a855986 100644 --- a/eslint.config.ts +++ b/eslint.config.ts @@ -1,36 +1,27 @@ -// import { defineConfig } from '@importantimport/eslint-config' +import { GLOB_MARKDOWN_CODE, GLOB_TESTS } from '@antfu/eslint-config' +import { defineConfig } from '@importantimport/eslint-config' -// export default defineConfig({ -// typescript: { tsconfigPath: './tsconfig.json' }, -// }) - -import antfu, { GLOB_MARKDOWN_CODE, GLOB_TESTS } from '@antfu/eslint-config' -import { ii } from '@importantimport/eslint-config' - -export default antfu({ typescript: { tsconfigPath: './tsconfig.json' } }) - .append(ii({ typescript: { tsconfigPath: './tsconfig.json' } })) - .append({ - ignores: [ - 'docs/components/ui/**/*.tsx', - ], - }) - .append({ - rules: { - '@masknet/no-default-error': 'off', - '@masknet/no-then': 'off', - 'sonarjs/todo-tag': 'warn', - }, - }) - .append({ - files: [...GLOB_TESTS, GLOB_MARKDOWN_CODE], - rules: { - '@masknet/no-top-level': 'off', - '@masknet/unicode-specific-set': 'off', - }, - }) - .append({ - ignores: [ - 'cspell.config.yaml', - 'cspell.config.yml', - ], - }) +export default defineConfig({ + typescript: { tsconfigPath: './tsconfig.json' }, +}, [{ + ignores: [ + 'docs/components/ui/**/*.tsx', + ], +}, { + rules: { + '@masknet/no-default-error': 'off', + '@masknet/no-then': 'off', + 'sonarjs/todo-tag': 'warn', + }, +}, { + files: [...GLOB_TESTS, GLOB_MARKDOWN_CODE], + rules: { + '@masknet/no-top-level': 'off', + '@masknet/unicode-specific-set': 'off', + }, +}, { + ignores: [ + 'cspell.config.yaml', + 'cspell.config.yml', + ], +}]) diff --git a/packages-top/xsai/package.json b/packages-top/xsai/package.json index 81764a7..98c9b93 100644 --- a/packages-top/xsai/package.json +++ b/packages-top/xsai/package.json @@ -42,6 +42,7 @@ }, "dependencies": { "@xsai/embed": "workspace:", + "@xsai/generate-image": "workspace:", "@xsai/generate-object": "workspace:", "@xsai/generate-speech": "workspace:", "@xsai/generate-text": "workspace:", diff --git a/packages-top/xsai/src/index.ts b/packages-top/xsai/src/index.ts index 069e5fd..004ba7b 100644 --- a/packages-top/xsai/src/index.ts +++ b/packages-top/xsai/src/index.ts @@ -1,4 +1,5 @@ export * from '@xsai/embed' +export * from '@xsai/generate-image' export * from '@xsai/generate-object' export * from '@xsai/generate-speech' export * from '@xsai/generate-text' diff --git a/packages/generate-image/README.md b/packages/generate-image/README.md new file mode 100644 index 0000000..39ef677 --- /dev/null +++ b/packages/generate-image/README.md @@ -0,0 +1 @@ +https://xsai.js.org/docs/packages/generate/image diff --git a/packages/generate-image/package.json b/packages/generate-image/package.json new file mode 100644 index 0000000..e00d805 --- /dev/null +++ b/packages/generate-image/package.json @@ -0,0 +1,42 @@ +{ + "name": "@xsai/generate-image", + "type": "module", + "version": "0.2.0-beta.1", + "description": "extra-small AI SDK for Browser, Node.js, Deno, Bun or Edge Runtime.", + "author": "Moeru AI", + "license": "MIT", + "homepage": "https://xsai.js.org", + "repository": { + "type": "git", + "url": "git+https://github.com/moeru-ai/xsai.git", + "directory": "packages/generate-image" + }, + "bugs": "https://github.com/moeru-ai/xsai/issues", + "keywords": [ + "xsai", + "openai", + "ai" + ], + "sideEffects": false, + "exports": "./src/index.ts", + "files": [ + "dist" + ], + "publishConfig": { + "exports": { + ".": { + "types": "./dist/index.d.ts", + "default": "./dist/index.js" + }, + "./package.json": "./package.json" + }, + "main": "./dist/index.js", + "types": "./dist/index.d.ts" + }, + "scripts": { + "build": "tsup" + }, + "dependencies": { + "@xsai/shared": "workspace:" + } +} diff --git a/packages/generate-image/src/index.ts b/packages/generate-image/src/index.ts new file mode 100644 index 0000000..1d1ae6e --- /dev/null +++ b/packages/generate-image/src/index.ts @@ -0,0 +1,79 @@ +import type { CommonRequestOptions } from '@xsai/shared' + +import { requestBody, requestHeaders, requestURL, responseJSON } from '@xsai/shared' + +export interface GenerateImageOptions extends CommonRequestOptions { + [key: string]: unknown + /** + * The number of images to generate. + * @default `1` + */ + n?: number + /** A text description of the desired image(s). */ + prompt: string + /** + * The size of the generated images. + * @default `1024x1024` + */ + size?: `${number}x${number}` +} + +export interface GenerateImageResponse { + created: number + data: { + b64_json: string + revised_prompt?: string + }[] +} + +export interface GenerateImageResult { + image: GenerateImageResultImage + images: GenerateImageResultImage[] +} + +export interface GenerateImageResultImage { + base64: string + mimeType: string +} + +/** @internal */ +const mimeTypes = { + '/9j/': 'image/jpg', + 'AAAAIGZ0eXBhdmlm': 'image/avif', + 'iVBORw0KGgo': 'image/png', + 'R0lGOD': 'image/gif', + 'UklGRg==': 'image/webp', +} + +/** @internal */ +const convertImage = (b64_json: string) => { + const key = Object.keys(mimeTypes).find(prefix => b64_json.startsWith(prefix)) as keyof typeof mimeTypes | undefined + const mimeType = mimeTypes[key ?? 'iVBORw0KGgo'] + + return { + // eslint-disable-next-line @masknet/string-no-data-url + base64: `data:${mimeType};base64,${b64_json}`, + mimeType, + } +} + +/** @experimental */ +export const generateImage = async (options: GenerateImageOptions): Promise => + (options.fetch ?? globalThis.fetch)(requestURL('images/generations', options.baseURL), { + body: requestBody({ + ...options, + responseFormat: 'b64_json', + }), + headers: requestHeaders({ + 'Content-Type': 'application/json', + ...options.headers, + }, options.apiKey), + method: 'POST', + signal: options.abortSignal, + }) + .then(responseJSON) + .then(({ data }) => { + const images: GenerateImageResultImage[] = data.map(({ b64_json }) => convertImage(b64_json)) + + return { image: images[0], images } + }) diff --git a/packages/generate-object/src/index.ts b/packages/generate-object/src/index.ts index 05cc312..eea96a9 100644 --- a/packages/generate-object/src/index.ts +++ b/packages/generate-object/src/index.ts @@ -19,6 +19,7 @@ type GenerateObjectOutputOption = 'array' | 'object' export async function generateObject(options: GenerateObjectOptions & { output: 'array' }): Promise>>> export async function generateObject(options: GenerateObjectOptions & { output: 'object' }): Promise>> export async function generateObject(options: GenerateObjectOptions): Promise>> +// eslint-disable-next-line func-style export async function generateObject(options: GenerateObjectOptions & { output?: GenerateObjectOutputOption }) { const { schema: schemaValidator } = options diff --git a/packages/stream-object/src/index.ts b/packages/stream-object/src/index.ts index c490a81..9981e53 100644 --- a/packages/stream-object/src/index.ts +++ b/packages/stream-object/src/index.ts @@ -43,18 +43,16 @@ export async function streamObject( & StreamObjectOptions & { output: 'array' } ): Promise & { elementStream: ReadableStream>, partialObjectStream: undefined }> - export async function streamObject( options: StreamObjectExtraOptions & StreamObjectOptions & { output: 'object' } ): Promise & { elementStream: undefined, partialObjectStream: ReadableStream>> }> - export async function streamObject( options: StreamObjectExtraOptions & StreamObjectOptions ): Promise & { elementStream: undefined, partialObjectStream: ReadableStream>> }> - +// eslint-disable-next-line func-style export async function streamObject( options: StreamObjectExtraOptions & StreamObjectOptions diff --git a/packages/utils-stream/src/to-async-iterator.ts b/packages/utils-stream/src/to-async-iterator.ts index ef22322..532f425 100644 --- a/packages/utils-stream/src/to-async-iterator.ts +++ b/packages/utils-stream/src/to-async-iterator.ts @@ -1,4 +1,5 @@ /** @see {@link https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream#browser_compatibility} */ +// eslint-disable-next-line func-style export async function* toAsyncIterator( stream: ReadableStream, ): AsyncGenerator { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 76472f3..c76f465 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -145,6 +145,9 @@ importers: '@xsai/embed': specifier: workspace:^ version: link:../packages/embed + '@xsai/generate-image': + specifier: workspace:^ + version: link:../packages/generate-image '@xsai/generate-object': specifier: workspace:^ version: link:../packages/generate-object @@ -258,6 +261,9 @@ importers: '@xsai/embed': specifier: 'workspace:' version: link:../../packages/embed + '@xsai/generate-image': + specifier: 'workspace:' + version: link:../../packages/generate-image '@xsai/generate-object': specifier: 'workspace:' version: link:../../packages/generate-object @@ -330,6 +336,12 @@ importers: specifier: 'workspace:' version: link:../shared + packages/generate-image: + dependencies: + '@xsai/shared': + specifier: 'workspace:' + version: link:../shared + packages/generate-object: dependencies: '@xsai/generate-text': diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 406ab16..4f9836a 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -10,12 +10,9 @@ catalog: catalogs: schema: - '@valibot/to-json-schema': ^1.0.0 - valibot: ^1.0.0 - + '@valibot/to-json-schema': &valibot ^1.0.0 arktype: ^2.1.13 - effect: ^3.14.2 - + valibot: *valibot zod: ^3.24.2 zod-to-json-schema: ^3.24.5