kopia lustrzana https://github.com/transitive-bullshit/chatgpt-api
feat: add unofficial midjourney client
rodzic
98202e5ea6
commit
0f296becb4
|
@ -8,7 +8,7 @@ import restoreCursor from 'restore-cursor'
|
||||||
// import { ProxycurlClient } from '../src/services/proxycurl-client.js'
|
// import { ProxycurlClient } from '../src/services/proxycurl-client.js'
|
||||||
// import { WikipediaClient } from '../src/index.js'
|
// import { WikipediaClient } from '../src/index.js'
|
||||||
// import { PerigonClient } from '../src/index.js'
|
// import { PerigonClient } from '../src/index.js'
|
||||||
import { FirecrawlClient } from '../src/index.js'
|
// import { FirecrawlClient } from '../src/index.js'
|
||||||
// import { ExaClient } from '../src/index.js'
|
// import { ExaClient } from '../src/index.js'
|
||||||
// import { DiffbotClient } from '../src/index.js'
|
// import { DiffbotClient } from '../src/index.js'
|
||||||
// import { WolframClient } from '../src/index.js'
|
// import { WolframClient } from '../src/index.js'
|
||||||
|
@ -16,6 +16,7 @@ import { FirecrawlClient } from '../src/index.js'
|
||||||
// createTwitterV2Client,
|
// createTwitterV2Client,
|
||||||
// TwitterClient
|
// TwitterClient
|
||||||
// } from '../src/services/twitter/index.js'
|
// } from '../src/services/twitter/index.js'
|
||||||
|
import { MidjourneyClient } from '../src/index.js'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Scratch pad for testing.
|
* Scratch pad for testing.
|
||||||
|
@ -56,13 +57,13 @@ async function main() {
|
||||||
// })
|
// })
|
||||||
// console.log(JSON.stringify(res, null, 2))
|
// console.log(JSON.stringify(res, null, 2))
|
||||||
|
|
||||||
const firecrawl = new FirecrawlClient()
|
// const firecrawl = new FirecrawlClient()
|
||||||
const res = await firecrawl.scrapeUrl({
|
// const res = await firecrawl.scrapeUrl({
|
||||||
url: 'https://www.bbc.com/news/articles/cp4475gwny1o'
|
// url: 'https://www.bbc.com/news/articles/cp4475gwny1o'
|
||||||
// url: 'https://www.theguardian.com/technology/article/2024/jun/04/openai-google-ai-risks-letter'
|
// // url: 'https://www.theguardian.com/technology/article/2024/jun/04/openai-google-ai-risks-letter'
|
||||||
// url: 'https://www.firecrawl.dev'
|
// // url: 'https://www.firecrawl.dev'
|
||||||
})
|
// })
|
||||||
console.log(JSON.stringify(res, null, 2))
|
// console.log(JSON.stringify(res, null, 2))
|
||||||
|
|
||||||
// const exa = new ExaClient()
|
// const exa = new ExaClient()
|
||||||
// const res = await exa.search({
|
// const res = await exa.search({
|
||||||
|
@ -96,6 +97,12 @@ async function main() {
|
||||||
// query: 'open source AI agents'
|
// query: 'open source AI agents'
|
||||||
// })
|
// })
|
||||||
// console.log(res)
|
// console.log(res)
|
||||||
|
|
||||||
|
const midjourney = new MidjourneyClient()
|
||||||
|
const res = await midjourney.imagine(
|
||||||
|
'tiny lil baby kittens playing with an inquisitive AI robot, kawaii, anime'
|
||||||
|
)
|
||||||
|
console.log(JSON.stringify(res, null, 2))
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
export class RetryableError extends Error {}
|
export class RetryableError extends Error {}
|
||||||
|
|
||||||
export class ParseError extends RetryableError {}
|
export class ParseError extends RetryableError {}
|
||||||
|
|
||||||
|
export class TimeoutError extends Error {}
|
||||||
|
|
|
@ -3,6 +3,7 @@ export * from './dexa-client.js'
|
||||||
export * from './diffbot-client.js'
|
export * from './diffbot-client.js'
|
||||||
export * from './exa-client.js'
|
export * from './exa-client.js'
|
||||||
export * from './firecrawl-client.js'
|
export * from './firecrawl-client.js'
|
||||||
|
export * from './midjourney-client.js'
|
||||||
export * from './people-data-labs-client.js'
|
export * from './people-data-labs-client.js'
|
||||||
export * from './perigon-client.js'
|
export * from './perigon-client.js'
|
||||||
export * from './predict-leads-client.js'
|
export * from './predict-leads-client.js'
|
||||||
|
|
|
@ -0,0 +1,139 @@
|
||||||
|
import defaultKy, { type KyInstance } from 'ky'
|
||||||
|
import { z } from 'zod'
|
||||||
|
|
||||||
|
import { TimeoutError } from '../errors.js'
|
||||||
|
import { aiFunction, AIFunctionsProvider } from '../fns.js'
|
||||||
|
import { assert, delay, getEnv, pruneNullOrUndefined } from '../utils.js'
|
||||||
|
|
||||||
|
export namespace midjourney {
|
||||||
|
export const API_BASE_URL = 'https://cl.imagineapi.dev'
|
||||||
|
|
||||||
|
export type JobStatus = 'pending' | 'in-progress' | 'completed' | 'failed'
|
||||||
|
|
||||||
|
export interface ImagineResponse {
|
||||||
|
data: Job
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Job {
|
||||||
|
id: string
|
||||||
|
prompt: string
|
||||||
|
status: JobStatus
|
||||||
|
user_created: string
|
||||||
|
date_created: string
|
||||||
|
results?: string
|
||||||
|
progress?: string
|
||||||
|
url?: string
|
||||||
|
error?: string
|
||||||
|
upscaled_urls?: string[]
|
||||||
|
ref?: string
|
||||||
|
upscaled?: string[]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unofficial Midjourney API client.
|
||||||
|
*
|
||||||
|
* @see https://www.imagineapi.dev
|
||||||
|
*/
|
||||||
|
export class MidjourneyClient extends AIFunctionsProvider {
|
||||||
|
readonly ky: KyInstance
|
||||||
|
readonly apiKey: string
|
||||||
|
readonly apiBaseUrl: string
|
||||||
|
|
||||||
|
constructor({
|
||||||
|
apiKey = getEnv('MIDJOURNEY_IMAGINE_API_KEY'),
|
||||||
|
apiBaseUrl = midjourney.API_BASE_URL,
|
||||||
|
ky = defaultKy
|
||||||
|
}: {
|
||||||
|
apiKey?: string
|
||||||
|
apiBaseUrl?: string
|
||||||
|
ky?: KyInstance
|
||||||
|
} = {}) {
|
||||||
|
assert(
|
||||||
|
apiKey,
|
||||||
|
'MidjourneyClient missing required "apiKey" (defaults to "MIDJOURNEY_IMAGINE_API_KEY")'
|
||||||
|
)
|
||||||
|
super()
|
||||||
|
|
||||||
|
this.apiKey = apiKey
|
||||||
|
this.apiBaseUrl = apiBaseUrl
|
||||||
|
|
||||||
|
this.ky = ky.extend({
|
||||||
|
prefixUrl: apiBaseUrl,
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${this.apiKey}`
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
@aiFunction({
|
||||||
|
name: 'midjourney_create_images',
|
||||||
|
description:
|
||||||
|
'Creates 4 images from a prompt using the Midjourney API. Useful for generating images on the fly.',
|
||||||
|
inputSchema: z.object({
|
||||||
|
prompt: z
|
||||||
|
.string()
|
||||||
|
.describe(
|
||||||
|
'Simple, short, comma-separated list of phrases which describe the image you want to generate'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
async imagine(
|
||||||
|
promptOrOptions: string | { prompt: string }
|
||||||
|
): Promise<midjourney.Job> {
|
||||||
|
const options =
|
||||||
|
typeof promptOrOptions === 'string'
|
||||||
|
? { prompt: promptOrOptions }
|
||||||
|
: promptOrOptions
|
||||||
|
|
||||||
|
const res = await this.ky
|
||||||
|
.post('items/images', {
|
||||||
|
json: { ...options }
|
||||||
|
})
|
||||||
|
.json<midjourney.ImagineResponse>()
|
||||||
|
|
||||||
|
return pruneNullOrUndefined(res.data)
|
||||||
|
}
|
||||||
|
|
||||||
|
async getJobById(jobId: string): Promise<midjourney.Job> {
|
||||||
|
const res = await this.ky
|
||||||
|
.get(`items/images/${jobId}`)
|
||||||
|
.json<midjourney.ImagineResponse>()
|
||||||
|
|
||||||
|
return pruneNullOrUndefined(res.data)
|
||||||
|
}
|
||||||
|
|
||||||
|
async waitForJobById(
|
||||||
|
jobId: string,
|
||||||
|
{
|
||||||
|
timeoutMs = 5 * 60 * 1000, // 5 minutes
|
||||||
|
intervalMs = 1000
|
||||||
|
}: {
|
||||||
|
timeoutMs?: number
|
||||||
|
intervalMs?: number
|
||||||
|
} = {}
|
||||||
|
) {
|
||||||
|
const startTimeMs = Date.now()
|
||||||
|
|
||||||
|
function checkForTimeout() {
|
||||||
|
const elapsedTimeMs = Date.now() - startTimeMs
|
||||||
|
if (elapsedTimeMs >= timeoutMs) {
|
||||||
|
throw new TimeoutError(
|
||||||
|
`MidjourneyClient timeout waiting for job "${jobId}"`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
do {
|
||||||
|
checkForTimeout()
|
||||||
|
|
||||||
|
const job = await this.getJobById(jobId)
|
||||||
|
if (job.status === 'completed' || job.status === 'failed') {
|
||||||
|
return job
|
||||||
|
}
|
||||||
|
|
||||||
|
checkForTimeout()
|
||||||
|
await delay(intervalMs)
|
||||||
|
} while (true)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,31 @@
|
||||||
|
import { describe, expect, test } from 'vitest'
|
||||||
|
|
||||||
|
import { normalizeUrl } from './url-utils.js'
|
||||||
|
|
||||||
|
describe('normalizeUrl', () => {
|
||||||
|
test('valid urls', async () => {
|
||||||
|
expect(normalizeUrl('https://www.google.com')).toBe(
|
||||||
|
'https://www.google.com'
|
||||||
|
)
|
||||||
|
expect(normalizeUrl('//www.google.com')).toBe('https://www.google.com')
|
||||||
|
expect(normalizeUrl('https://www.google.com/foo?')).toBe(
|
||||||
|
'https://www.google.com/foo'
|
||||||
|
)
|
||||||
|
expect(normalizeUrl('https://www.google.com/?foo=bar&dog=cat')).toBe(
|
||||||
|
'https://www.google.com/?dog=cat&foo=bar'
|
||||||
|
)
|
||||||
|
expect(normalizeUrl('https://google.com/abc/123//')).toBe(
|
||||||
|
'https://google.com/abc/123'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('invalid urls', async () => {
|
||||||
|
expect(normalizeUrl('/foo')).toBe(null)
|
||||||
|
expect(normalizeUrl('/foo/bar/baz')).toBe(null)
|
||||||
|
expect(normalizeUrl('://foo.com')).toBe(null)
|
||||||
|
expect(normalizeUrl('foo')).toBe(null)
|
||||||
|
expect(normalizeUrl('')).toBe(null)
|
||||||
|
expect(normalizeUrl(undefined as unknown as string)).toBe(null)
|
||||||
|
expect(normalizeUrl(null as unknown as string)).toBe(null)
|
||||||
|
})
|
||||||
|
})
|
10
src/utils.ts
10
src/utils.ts
|
@ -57,6 +57,16 @@ export function pruneUndefined<T extends Record<string, any>>(
|
||||||
) as NonNullable<T>
|
) as NonNullable<T>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function pruneNullOrUndefined<T extends Record<string, any>>(
|
||||||
|
obj: T
|
||||||
|
): NonNullable<{ [K in keyof T]: Exclude<T[K], undefined | null> }> {
|
||||||
|
return Object.fromEntries(
|
||||||
|
Object.entries(obj).filter(
|
||||||
|
([, value]) => value !== undefined && value !== null
|
||||||
|
)
|
||||||
|
) as NonNullable<T>
|
||||||
|
}
|
||||||
|
|
||||||
export function getEnv(name: string): string | undefined {
|
export function getEnv(name: string): string | undefined {
|
||||||
try {
|
try {
|
||||||
return typeof process !== 'undefined'
|
return typeof process !== 'undefined'
|
||||||
|
|
Ładowanie…
Reference in New Issue