hazo_images 1.1.0 → 1.2.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/CHANGE_LOG.md +15 -0
- package/README.md +45 -1
- package/SETUP_CHECKLIST.md +9 -2
- package/dist/runware/index.d.ts +83 -0
- package/dist/runware/index.js +430 -0
- package/package.json +14 -3
package/CHANGE_LOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
# hazo_images Change Log
|
|
2
2
|
|
|
3
|
+
## 1.2.0 — 2026-05-30
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- New `hazo_images/runware` sub-export wrapping the Runware REST API.
|
|
7
|
+
- `createRunwareClient(opts?)` factory — `generateImage(params)` and `getBalance()`.
|
|
8
|
+
- `assemblePrompts(input)` helper for the prefix + character + image + suffix pattern.
|
|
9
|
+
- Four `HazoError` subclasses: `RunwareApiError`, `RunwareAuthError`, `RunwareRateLimitError` (carries `retryAfter`), `RunwareValidationError`. All codes are `HAZO_IMAGES_RUNWARE_*`.
|
|
10
|
+
- API key resolution: `opts.apiKey` → `HAZO_IMAGES_RUNWARE_API_KEY` → `RUNWARE_API_KEY` (legacy).
|
|
11
|
+
- Outbound HTTP goes through `fetchWithRequestId` from `hazo_core` (first outbound HTTP in this package).
|
|
12
|
+
- Configurable `baseUrl`, `defaultModel`, `timeoutMs` (default 60s), `fetch` (override for tests), `logger`.
|
|
13
|
+
|
|
14
|
+
### Migration
|
|
15
|
+
- Extracted verbatim from `14.seo_monitor/lib/runware/{client,resolve-params}.ts`.
|
|
16
|
+
- seo_monitor migrated to consume `hazo_images/runware` in the same release; seo_monitor's old `lib/runware/client.ts` deleted.
|
|
17
|
+
|
|
3
18
|
## 1.1.0 (2026-05-29) — Wave 2 standardisation
|
|
4
19
|
|
|
5
20
|
### Changed
|
package/README.md
CHANGED
|
@@ -16,7 +16,7 @@ npm install hazo_core
|
|
|
16
16
|
npm install sharp
|
|
17
17
|
|
|
18
18
|
# Optional — only needed if using uploadProcessedImage
|
|
19
|
-
npm install hazo_files
|
|
19
|
+
npm install hazo_files
|
|
20
20
|
```
|
|
21
21
|
|
|
22
22
|
## Usage
|
|
@@ -153,6 +153,50 @@ interface ProcessImageOptions {
|
|
|
153
153
|
}
|
|
154
154
|
```
|
|
155
155
|
|
|
156
|
+
## Runware (AI image generation)
|
|
157
|
+
|
|
158
|
+
`hazo_images/runware` wraps the Runware REST API behind a factory client.
|
|
159
|
+
|
|
160
|
+
```ts
|
|
161
|
+
import { createRunwareClient } from 'hazo_images/runware';
|
|
162
|
+
|
|
163
|
+
const client = createRunwareClient({
|
|
164
|
+
// apiKey?: defaults to HAZO_IMAGES_RUNWARE_API_KEY then RUNWARE_API_KEY
|
|
165
|
+
defaultModel: 'runware:z-image@turbo',
|
|
166
|
+
timeoutMs: 60_000,
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
const { imageURL, cost, taskUUID } = await client.generateImage({
|
|
170
|
+
positivePrompt: 'a chibi explorer with a stopwatch',
|
|
171
|
+
width: 1024,
|
|
172
|
+
height: 1024,
|
|
173
|
+
});
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
### Error classes
|
|
177
|
+
|
|
178
|
+
| Class | Triggered by | Code(s) |
|
|
179
|
+
|---|---|---|
|
|
180
|
+
| `RunwareApiError` | network fail, 5xx, abort, empty result | `HAZO_IMAGES_RUNWARE_HTTP_FAILURE`, `HAZO_IMAGES_RUNWARE_TIMEOUT`, `HAZO_IMAGES_RUNWARE_EMPTY_RESULT` |
|
|
181
|
+
| `RunwareAuthError` | missing key, 401, 403 | `HAZO_IMAGES_RUNWARE_MISSING_KEY`, `HAZO_IMAGES_RUNWARE_AUTH_FAILED` |
|
|
182
|
+
| `RunwareRateLimitError` | 429 — exposes `retryAfter: number \| null` | `HAZO_IMAGES_RUNWARE_RATE_LIMITED` |
|
|
183
|
+
| `RunwareValidationError` | 400 or `errors[]` in body | upstream code or `HAZO_IMAGES_RUNWARE_BAD_REQUEST` |
|
|
184
|
+
|
|
185
|
+
### `assemblePrompts`
|
|
186
|
+
|
|
187
|
+
Generic helper for the prefix + character + image + suffix pattern:
|
|
188
|
+
|
|
189
|
+
```ts
|
|
190
|
+
import { assemblePrompts } from 'hazo_images/runware';
|
|
191
|
+
|
|
192
|
+
const { positive, negative } = assemblePrompts({
|
|
193
|
+
imagePrompt: 'astronaut on mars',
|
|
194
|
+
masterStylePrefix: 'cinematic,',
|
|
195
|
+
characterPositive: 'red spacesuit',
|
|
196
|
+
masterNegative: 'low quality, blurry',
|
|
197
|
+
});
|
|
198
|
+
```
|
|
199
|
+
|
|
156
200
|
## Roadmap
|
|
157
201
|
|
|
158
202
|
| Version | Scope |
|
package/SETUP_CHECKLIST.md
CHANGED
|
@@ -58,7 +58,14 @@ const nextConfig = {
|
|
|
58
58
|
module.exports = nextConfig;
|
|
59
59
|
```
|
|
60
60
|
|
|
61
|
-
## 6. (Optional)
|
|
61
|
+
## 6. (Optional) Environment variables for hazo_images/runware
|
|
62
|
+
|
|
63
|
+
Only needed if using the `hazo_images/runware` sub-export (Runware REST API client):
|
|
64
|
+
|
|
65
|
+
- `HAZO_IMAGES_RUNWARE_API_KEY` (preferred) — API key from [runware.ai](https://runware.ai)
|
|
66
|
+
- `RUNWARE_API_KEY` (legacy fallback) — older projects may use this; new code should prefer `HAZO_IMAGES_RUNWARE_API_KEY`
|
|
67
|
+
|
|
68
|
+
## 7. (Optional) Drop in `hazo_images_config.ini`
|
|
62
69
|
|
|
63
70
|
The package ships `config/hazo_images_config.ini.sample` with sections
|
|
64
71
|
for `[general]`, `[log.overrides]`, and `[processing]` defaults. Copy
|
|
@@ -71,7 +78,7 @@ Per `D-020`, a per-environment overlay file at
|
|
|
71
78
|
`config/hazo_images_config.<HAZO_ENV>.ini` is layered on top of the
|
|
72
79
|
base file when `HAZO_ENV` is set.
|
|
73
80
|
|
|
74
|
-
##
|
|
81
|
+
## 8. Verify
|
|
75
82
|
|
|
76
83
|
```ts
|
|
77
84
|
import { processImage } from 'hazo_images/server';
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { HazoCoreLogger, HazoError } from 'hazo_core';
|
|
2
|
+
|
|
3
|
+
interface CreateRunwareClientOptions {
|
|
4
|
+
apiKey?: string;
|
|
5
|
+
baseUrl?: string;
|
|
6
|
+
defaultModel?: string;
|
|
7
|
+
timeoutMs?: number;
|
|
8
|
+
fetch?: typeof fetch;
|
|
9
|
+
logger?: HazoCoreLogger;
|
|
10
|
+
}
|
|
11
|
+
interface GenerateImageParams {
|
|
12
|
+
positivePrompt: string;
|
|
13
|
+
negativePrompt?: string;
|
|
14
|
+
model?: string;
|
|
15
|
+
width: number;
|
|
16
|
+
height: number;
|
|
17
|
+
numberResults?: number;
|
|
18
|
+
outputFormat?: 'PNG' | 'JPEG' | 'WEBP';
|
|
19
|
+
outputType?: 'URL' | 'base64Data';
|
|
20
|
+
}
|
|
21
|
+
interface GenerateImageResult {
|
|
22
|
+
imageURL?: string;
|
|
23
|
+
imageBase64Data?: string;
|
|
24
|
+
cost: number | null;
|
|
25
|
+
taskUUID: string;
|
|
26
|
+
}
|
|
27
|
+
interface RunwareClient {
|
|
28
|
+
generateImage(params: GenerateImageParams): Promise<GenerateImageResult>;
|
|
29
|
+
getBalance(): Promise<number>;
|
|
30
|
+
}
|
|
31
|
+
interface AssemblePromptsInput {
|
|
32
|
+
imagePrompt: string;
|
|
33
|
+
masterStylePrefix?: string | null;
|
|
34
|
+
masterStyleSuffix?: string | null;
|
|
35
|
+
characterPositive?: string | null;
|
|
36
|
+
masterNegative?: string | null;
|
|
37
|
+
characterNegative?: string | null;
|
|
38
|
+
callNegative?: string | null;
|
|
39
|
+
}
|
|
40
|
+
interface ResolvedPrompts {
|
|
41
|
+
positive: string;
|
|
42
|
+
negative: string | undefined;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
declare function createRunwareClient(opts?: CreateRunwareClientOptions): RunwareClient;
|
|
46
|
+
|
|
47
|
+
declare function assemblePrompts(input: AssemblePromptsInput): ResolvedPrompts;
|
|
48
|
+
|
|
49
|
+
declare const RUNWARE_ERROR_CODES: {
|
|
50
|
+
readonly MISSING_KEY: "HAZO_IMAGES_RUNWARE_MISSING_KEY";
|
|
51
|
+
readonly AUTH_FAILED: "HAZO_IMAGES_RUNWARE_AUTH_FAILED";
|
|
52
|
+
readonly RATE_LIMITED: "HAZO_IMAGES_RUNWARE_RATE_LIMITED";
|
|
53
|
+
readonly HTTP_FAILURE: "HAZO_IMAGES_RUNWARE_HTTP_FAILURE";
|
|
54
|
+
readonly TIMEOUT: "HAZO_IMAGES_RUNWARE_TIMEOUT";
|
|
55
|
+
readonly EMPTY_RESULT: "HAZO_IMAGES_RUNWARE_EMPTY_RESULT";
|
|
56
|
+
readonly BAD_REQUEST: "HAZO_IMAGES_RUNWARE_BAD_REQUEST";
|
|
57
|
+
readonly MISSING_MODEL: "HAZO_IMAGES_RUNWARE_MISSING_MODEL";
|
|
58
|
+
};
|
|
59
|
+
type RunwareErrorCode = (typeof RUNWARE_ERROR_CODES)[keyof typeof RUNWARE_ERROR_CODES];
|
|
60
|
+
interface RunwareErrorContext {
|
|
61
|
+
status?: number;
|
|
62
|
+
body?: unknown;
|
|
63
|
+
taskUUID?: string;
|
|
64
|
+
code?: string;
|
|
65
|
+
cause?: unknown;
|
|
66
|
+
}
|
|
67
|
+
declare class RunwareApiError extends HazoError {
|
|
68
|
+
constructor(code: string, message: string, context?: RunwareErrorContext);
|
|
69
|
+
}
|
|
70
|
+
declare class RunwareAuthError extends HazoError {
|
|
71
|
+
constructor(code: string, message: string, context?: RunwareErrorContext);
|
|
72
|
+
}
|
|
73
|
+
declare class RunwareRateLimitError extends HazoError {
|
|
74
|
+
retryAfter: number | null;
|
|
75
|
+
constructor(message: string, context: RunwareErrorContext & {
|
|
76
|
+
retryAfter: number | null;
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
declare class RunwareValidationError extends HazoError {
|
|
80
|
+
constructor(code: string, message: string, context?: RunwareErrorContext);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export { type AssemblePromptsInput, type CreateRunwareClientOptions, type GenerateImageParams, type GenerateImageResult, RUNWARE_ERROR_CODES, type ResolvedPrompts, RunwareApiError, RunwareAuthError, type RunwareClient, type RunwareErrorCode, RunwareRateLimitError, RunwareValidationError, assemblePrompts, createRunwareClient };
|
|
@@ -0,0 +1,430 @@
|
|
|
1
|
+
// src/runware/client.ts
|
|
2
|
+
import { fetchWithRequestId } from "hazo_core";
|
|
3
|
+
|
|
4
|
+
// ../node_modules/uuid/dist/esm-node/rng.js
|
|
5
|
+
import crypto from "crypto";
|
|
6
|
+
var rnds8Pool = new Uint8Array(256);
|
|
7
|
+
var poolPtr = rnds8Pool.length;
|
|
8
|
+
function rng() {
|
|
9
|
+
if (poolPtr > rnds8Pool.length - 16) {
|
|
10
|
+
crypto.randomFillSync(rnds8Pool);
|
|
11
|
+
poolPtr = 0;
|
|
12
|
+
}
|
|
13
|
+
return rnds8Pool.slice(poolPtr, poolPtr += 16);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// ../node_modules/uuid/dist/esm-node/regex.js
|
|
17
|
+
var regex_default = /^(?:[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}|00000000-0000-0000-0000-000000000000)$/i;
|
|
18
|
+
|
|
19
|
+
// ../node_modules/uuid/dist/esm-node/validate.js
|
|
20
|
+
function validate(uuid) {
|
|
21
|
+
return typeof uuid === "string" && regex_default.test(uuid);
|
|
22
|
+
}
|
|
23
|
+
var validate_default = validate;
|
|
24
|
+
|
|
25
|
+
// ../node_modules/uuid/dist/esm-node/stringify.js
|
|
26
|
+
var byteToHex = [];
|
|
27
|
+
for (let i = 0; i < 256; ++i) {
|
|
28
|
+
byteToHex.push((i + 256).toString(16).substr(1));
|
|
29
|
+
}
|
|
30
|
+
function stringify(arr, offset = 0) {
|
|
31
|
+
const uuid = (byteToHex[arr[offset + 0]] + byteToHex[arr[offset + 1]] + byteToHex[arr[offset + 2]] + byteToHex[arr[offset + 3]] + "-" + byteToHex[arr[offset + 4]] + byteToHex[arr[offset + 5]] + "-" + byteToHex[arr[offset + 6]] + byteToHex[arr[offset + 7]] + "-" + byteToHex[arr[offset + 8]] + byteToHex[arr[offset + 9]] + "-" + byteToHex[arr[offset + 10]] + byteToHex[arr[offset + 11]] + byteToHex[arr[offset + 12]] + byteToHex[arr[offset + 13]] + byteToHex[arr[offset + 14]] + byteToHex[arr[offset + 15]]).toLowerCase();
|
|
32
|
+
if (!validate_default(uuid)) {
|
|
33
|
+
throw TypeError("Stringified UUID is invalid");
|
|
34
|
+
}
|
|
35
|
+
return uuid;
|
|
36
|
+
}
|
|
37
|
+
var stringify_default = stringify;
|
|
38
|
+
|
|
39
|
+
// ../node_modules/uuid/dist/esm-node/v4.js
|
|
40
|
+
function v4(options, buf, offset) {
|
|
41
|
+
options = options || {};
|
|
42
|
+
const rnds = options.random || (options.rng || rng)();
|
|
43
|
+
rnds[6] = rnds[6] & 15 | 64;
|
|
44
|
+
rnds[8] = rnds[8] & 63 | 128;
|
|
45
|
+
if (buf) {
|
|
46
|
+
offset = offset || 0;
|
|
47
|
+
for (let i = 0; i < 16; ++i) {
|
|
48
|
+
buf[offset + i] = rnds[i];
|
|
49
|
+
}
|
|
50
|
+
return buf;
|
|
51
|
+
}
|
|
52
|
+
return stringify_default(rnds);
|
|
53
|
+
}
|
|
54
|
+
var v4_default = v4;
|
|
55
|
+
|
|
56
|
+
// src/utils/uuid.ts
|
|
57
|
+
import { generateRequestId } from "hazo_core";
|
|
58
|
+
var uuidv4 = v4_default;
|
|
59
|
+
function generateTaskUUID() {
|
|
60
|
+
return uuidv4();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// src/utils/logger.ts
|
|
64
|
+
import { createLogger } from "hazo_core";
|
|
65
|
+
var console_logger = {
|
|
66
|
+
info: (m, d) => d ? console.log(`[hazo_images] ${m}`, d) : console.log(`[hazo_images] ${m}`),
|
|
67
|
+
debug: (m, d) => d ? console.debug(`[hazo_images] ${m}`, d) : console.debug(`[hazo_images] ${m}`),
|
|
68
|
+
warn: (m, d) => d ? console.warn(`[hazo_images] ${m}`, d) : console.warn(`[hazo_images] ${m}`),
|
|
69
|
+
error: (m, d) => d ? console.error(`[hazo_images] ${m}`, d) : console.error(`[hazo_images] ${m}`)
|
|
70
|
+
};
|
|
71
|
+
function build_default_logger() {
|
|
72
|
+
try {
|
|
73
|
+
return createLogger("hazo_images");
|
|
74
|
+
} catch {
|
|
75
|
+
return console_logger;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
var current_logger = null;
|
|
79
|
+
function get_logger() {
|
|
80
|
+
if (!current_logger) {
|
|
81
|
+
current_logger = build_default_logger();
|
|
82
|
+
}
|
|
83
|
+
return current_logger;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// src/runware/errors.ts
|
|
87
|
+
import { HazoError } from "hazo_core";
|
|
88
|
+
var RUNWARE_ERROR_CODES = {
|
|
89
|
+
MISSING_KEY: "HAZO_IMAGES_RUNWARE_MISSING_KEY",
|
|
90
|
+
AUTH_FAILED: "HAZO_IMAGES_RUNWARE_AUTH_FAILED",
|
|
91
|
+
RATE_LIMITED: "HAZO_IMAGES_RUNWARE_RATE_LIMITED",
|
|
92
|
+
HTTP_FAILURE: "HAZO_IMAGES_RUNWARE_HTTP_FAILURE",
|
|
93
|
+
TIMEOUT: "HAZO_IMAGES_RUNWARE_TIMEOUT",
|
|
94
|
+
EMPTY_RESULT: "HAZO_IMAGES_RUNWARE_EMPTY_RESULT",
|
|
95
|
+
BAD_REQUEST: "HAZO_IMAGES_RUNWARE_BAD_REQUEST",
|
|
96
|
+
MISSING_MODEL: "HAZO_IMAGES_RUNWARE_MISSING_MODEL"
|
|
97
|
+
};
|
|
98
|
+
var RunwareApiError = class extends HazoError {
|
|
99
|
+
constructor(code, message, context = {}) {
|
|
100
|
+
super({ code, pkg: "hazo_images", message, context });
|
|
101
|
+
this.name = "RunwareApiError";
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
var RunwareAuthError = class extends HazoError {
|
|
105
|
+
constructor(code, message, context = {}) {
|
|
106
|
+
super({ code, pkg: "hazo_images", message, context });
|
|
107
|
+
this.name = "RunwareAuthError";
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
var RunwareRateLimitError = class extends HazoError {
|
|
111
|
+
retryAfter;
|
|
112
|
+
constructor(message, context) {
|
|
113
|
+
const { retryAfter, ...rest } = context;
|
|
114
|
+
super({
|
|
115
|
+
code: RUNWARE_ERROR_CODES.RATE_LIMITED,
|
|
116
|
+
pkg: "hazo_images",
|
|
117
|
+
message,
|
|
118
|
+
context: { ...rest, retryAfter }
|
|
119
|
+
});
|
|
120
|
+
this.name = "RunwareRateLimitError";
|
|
121
|
+
this.retryAfter = retryAfter;
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
var RunwareValidationError = class extends HazoError {
|
|
125
|
+
constructor(code, message, context = {}) {
|
|
126
|
+
super({ code, pkg: "hazo_images", message, context });
|
|
127
|
+
this.name = "RunwareValidationError";
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
// src/runware/client.ts
|
|
132
|
+
var DEFAULT_BASE_URL = "https://api.runware.ai/v1";
|
|
133
|
+
var DEFAULT_TIMEOUT_MS = 6e4;
|
|
134
|
+
function resolveApiKey(optsApiKey) {
|
|
135
|
+
if (optsApiKey) return optsApiKey;
|
|
136
|
+
if (process.env.HAZO_IMAGES_RUNWARE_API_KEY) return process.env.HAZO_IMAGES_RUNWARE_API_KEY;
|
|
137
|
+
if (process.env.RUNWARE_API_KEY) return process.env.RUNWARE_API_KEY;
|
|
138
|
+
throw new RunwareAuthError(
|
|
139
|
+
RUNWARE_ERROR_CODES.MISSING_KEY,
|
|
140
|
+
"Runware API key not found. Set HAZO_IMAGES_RUNWARE_API_KEY or RUNWARE_API_KEY, or pass apiKey to createRunwareClient."
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
function createRunwareClient(opts = {}) {
|
|
144
|
+
const logger = opts.logger ?? get_logger();
|
|
145
|
+
const cfg = {
|
|
146
|
+
baseUrl: opts.baseUrl ?? DEFAULT_BASE_URL,
|
|
147
|
+
defaultModel: opts.defaultModel,
|
|
148
|
+
timeoutMs: opts.timeoutMs ?? DEFAULT_TIMEOUT_MS,
|
|
149
|
+
fetchImpl: opts.fetch ?? fetchWithRequestId,
|
|
150
|
+
optsApiKey: opts.apiKey,
|
|
151
|
+
logger
|
|
152
|
+
};
|
|
153
|
+
logger.info("runware.client.created", {
|
|
154
|
+
baseUrl: cfg.baseUrl,
|
|
155
|
+
defaultModel: cfg.defaultModel ?? null,
|
|
156
|
+
timeoutMs: cfg.timeoutMs
|
|
157
|
+
});
|
|
158
|
+
return {
|
|
159
|
+
async generateImage(params) {
|
|
160
|
+
const start = Date.now();
|
|
161
|
+
const apiKey = resolveApiKey(cfg.optsApiKey);
|
|
162
|
+
const model = params.model ?? cfg.defaultModel;
|
|
163
|
+
if (!model) {
|
|
164
|
+
const err = new RunwareValidationError(
|
|
165
|
+
RUNWARE_ERROR_CODES.MISSING_MODEL,
|
|
166
|
+
"Runware generateImage requires a `model` (pass per-call or as defaultModel to createRunwareClient).",
|
|
167
|
+
{ taskUUID: "pre-task" }
|
|
168
|
+
);
|
|
169
|
+
cfg.logger.error("runware.generate_image.failed", {
|
|
170
|
+
errorCode: err.code,
|
|
171
|
+
errorClass: err.name,
|
|
172
|
+
durationMs: Date.now() - start
|
|
173
|
+
});
|
|
174
|
+
throw err;
|
|
175
|
+
}
|
|
176
|
+
const taskUUID = generateTaskUUID();
|
|
177
|
+
cfg.logger.info("runware.generate_image.started", { model, taskUUID });
|
|
178
|
+
const task = {
|
|
179
|
+
taskType: "imageInference",
|
|
180
|
+
taskUUID,
|
|
181
|
+
positivePrompt: params.positivePrompt,
|
|
182
|
+
...params.negativePrompt ? { negativePrompt: params.negativePrompt } : {},
|
|
183
|
+
model,
|
|
184
|
+
width: params.width,
|
|
185
|
+
height: params.height,
|
|
186
|
+
numberResults: params.numberResults ?? 1,
|
|
187
|
+
outputFormat: params.outputFormat ?? "PNG",
|
|
188
|
+
outputType: params.outputType ?? "URL"
|
|
189
|
+
};
|
|
190
|
+
const controller = new AbortController();
|
|
191
|
+
const timer = setTimeout(() => controller.abort(), cfg.timeoutMs);
|
|
192
|
+
let res;
|
|
193
|
+
try {
|
|
194
|
+
res = await cfg.fetchImpl(cfg.baseUrl, {
|
|
195
|
+
method: "POST",
|
|
196
|
+
headers: {
|
|
197
|
+
"Content-Type": "application/json",
|
|
198
|
+
Authorization: `Bearer ${apiKey}`
|
|
199
|
+
},
|
|
200
|
+
body: JSON.stringify([task]),
|
|
201
|
+
signal: controller.signal
|
|
202
|
+
});
|
|
203
|
+
} catch (err) {
|
|
204
|
+
clearTimeout(timer);
|
|
205
|
+
let thrownErr;
|
|
206
|
+
if (err instanceof DOMException && err.name === "AbortError") {
|
|
207
|
+
thrownErr = new RunwareApiError(
|
|
208
|
+
RUNWARE_ERROR_CODES.TIMEOUT,
|
|
209
|
+
`Runware request timed out after ${cfg.timeoutMs}ms`,
|
|
210
|
+
{ taskUUID }
|
|
211
|
+
);
|
|
212
|
+
} else {
|
|
213
|
+
thrownErr = new RunwareApiError(
|
|
214
|
+
RUNWARE_ERROR_CODES.HTTP_FAILURE,
|
|
215
|
+
"Runware network error",
|
|
216
|
+
{ taskUUID }
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
cfg.logger.error("runware.generate_image.failed", {
|
|
220
|
+
taskUUID,
|
|
221
|
+
model,
|
|
222
|
+
errorCode: thrownErr.code,
|
|
223
|
+
errorClass: thrownErr.name,
|
|
224
|
+
durationMs: Date.now() - start
|
|
225
|
+
});
|
|
226
|
+
throw thrownErr;
|
|
227
|
+
}
|
|
228
|
+
clearTimeout(timer);
|
|
229
|
+
if (!res.ok) {
|
|
230
|
+
let bodyText;
|
|
231
|
+
let bodyJson;
|
|
232
|
+
try {
|
|
233
|
+
bodyText = await res.clone().text();
|
|
234
|
+
bodyJson = JSON.parse(bodyText);
|
|
235
|
+
} catch {
|
|
236
|
+
bodyJson = bodyText;
|
|
237
|
+
}
|
|
238
|
+
const errorRecord = bodyJson?.errors?.[0];
|
|
239
|
+
let thrownErr;
|
|
240
|
+
if (res.status === 401 || res.status === 403) {
|
|
241
|
+
thrownErr = new RunwareAuthError(
|
|
242
|
+
RUNWARE_ERROR_CODES.AUTH_FAILED,
|
|
243
|
+
`Runware auth failed (HTTP ${res.status})`,
|
|
244
|
+
{ status: res.status, body: bodyJson, taskUUID }
|
|
245
|
+
);
|
|
246
|
+
} else if (res.status === 429) {
|
|
247
|
+
const ra = res.headers.get("retry-after");
|
|
248
|
+
const retryAfter = ra && /^\d+$/.test(ra) ? parseInt(ra, 10) : null;
|
|
249
|
+
thrownErr = new RunwareRateLimitError(`Runware rate limited (HTTP 429)`, {
|
|
250
|
+
status: 429,
|
|
251
|
+
body: bodyJson,
|
|
252
|
+
taskUUID,
|
|
253
|
+
retryAfter
|
|
254
|
+
});
|
|
255
|
+
} else if (res.status === 400 || errorRecord) {
|
|
256
|
+
thrownErr = new RunwareValidationError(
|
|
257
|
+
errorRecord?.code ? `HAZO_IMAGES_RUNWARE_${errorRecord.code.toUpperCase()}` : RUNWARE_ERROR_CODES.BAD_REQUEST,
|
|
258
|
+
errorRecord?.message ?? `Runware validation failed (HTTP ${res.status})`,
|
|
259
|
+
{ status: res.status, body: bodyJson, taskUUID, code: errorRecord?.code }
|
|
260
|
+
);
|
|
261
|
+
} else {
|
|
262
|
+
thrownErr = new RunwareApiError(
|
|
263
|
+
RUNWARE_ERROR_CODES.HTTP_FAILURE,
|
|
264
|
+
`Runware HTTP ${res.status}`,
|
|
265
|
+
{ status: res.status, body: bodyJson, taskUUID }
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
cfg.logger.error("runware.generate_image.failed", {
|
|
269
|
+
taskUUID,
|
|
270
|
+
model,
|
|
271
|
+
errorCode: thrownErr.code,
|
|
272
|
+
errorClass: thrownErr.name,
|
|
273
|
+
durationMs: Date.now() - start
|
|
274
|
+
});
|
|
275
|
+
throw thrownErr;
|
|
276
|
+
}
|
|
277
|
+
const body = await res.json();
|
|
278
|
+
const apiErrors = body.errors;
|
|
279
|
+
if (apiErrors && apiErrors.length > 0) {
|
|
280
|
+
const firstErr = apiErrors[0];
|
|
281
|
+
const thrownErr = new RunwareValidationError(
|
|
282
|
+
firstErr.code ? `HAZO_IMAGES_RUNWARE_${firstErr.code.toUpperCase()}` : RUNWARE_ERROR_CODES.BAD_REQUEST,
|
|
283
|
+
firstErr.message ?? "Runware returned an API error",
|
|
284
|
+
{ body, taskUUID, code: firstErr.code }
|
|
285
|
+
);
|
|
286
|
+
cfg.logger.error("runware.generate_image.failed", {
|
|
287
|
+
taskUUID,
|
|
288
|
+
model,
|
|
289
|
+
errorCode: thrownErr.code,
|
|
290
|
+
errorClass: thrownErr.name,
|
|
291
|
+
durationMs: Date.now() - start
|
|
292
|
+
});
|
|
293
|
+
throw thrownErr;
|
|
294
|
+
}
|
|
295
|
+
const first = body.data?.[0];
|
|
296
|
+
if (!first?.imageURL && !first?.imageBase64Data) {
|
|
297
|
+
const thrownErr = new RunwareApiError(
|
|
298
|
+
RUNWARE_ERROR_CODES.EMPTY_RESULT,
|
|
299
|
+
"Runware returned no image data",
|
|
300
|
+
{ body, taskUUID }
|
|
301
|
+
);
|
|
302
|
+
cfg.logger.error("runware.generate_image.failed", {
|
|
303
|
+
taskUUID,
|
|
304
|
+
model,
|
|
305
|
+
errorCode: thrownErr.code,
|
|
306
|
+
errorClass: thrownErr.name,
|
|
307
|
+
durationMs: Date.now() - start
|
|
308
|
+
});
|
|
309
|
+
throw thrownErr;
|
|
310
|
+
}
|
|
311
|
+
const result = {
|
|
312
|
+
imageURL: first.imageURL,
|
|
313
|
+
imageBase64Data: first.imageBase64Data,
|
|
314
|
+
cost: typeof first.cost === "number" ? first.cost : null,
|
|
315
|
+
taskUUID
|
|
316
|
+
};
|
|
317
|
+
cfg.logger.info("runware.generate_image.completed", {
|
|
318
|
+
taskUUID,
|
|
319
|
+
model,
|
|
320
|
+
cost: result.cost,
|
|
321
|
+
durationMs: Date.now() - start
|
|
322
|
+
});
|
|
323
|
+
return result;
|
|
324
|
+
},
|
|
325
|
+
async getBalance() {
|
|
326
|
+
const start = Date.now();
|
|
327
|
+
const apiKey = resolveApiKey(cfg.optsApiKey);
|
|
328
|
+
const taskUUID = generateTaskUUID();
|
|
329
|
+
const controller = new AbortController();
|
|
330
|
+
const timer = setTimeout(() => controller.abort(), cfg.timeoutMs);
|
|
331
|
+
let res;
|
|
332
|
+
try {
|
|
333
|
+
res = await cfg.fetchImpl(cfg.baseUrl, {
|
|
334
|
+
method: "POST",
|
|
335
|
+
headers: {
|
|
336
|
+
"Content-Type": "application/json",
|
|
337
|
+
Authorization: `Bearer ${apiKey}`
|
|
338
|
+
},
|
|
339
|
+
body: JSON.stringify([{ taskType: "getUser", taskUUID }]),
|
|
340
|
+
signal: controller.signal
|
|
341
|
+
});
|
|
342
|
+
} catch (err) {
|
|
343
|
+
clearTimeout(timer);
|
|
344
|
+
if (err instanceof DOMException && err.name === "AbortError") {
|
|
345
|
+
const thrownErr2 = new RunwareApiError(
|
|
346
|
+
RUNWARE_ERROR_CODES.TIMEOUT,
|
|
347
|
+
`Runware getBalance timed out`,
|
|
348
|
+
{ taskUUID }
|
|
349
|
+
);
|
|
350
|
+
cfg.logger.error("runware.get_balance.failed", {
|
|
351
|
+
taskUUID,
|
|
352
|
+
errorCode: thrownErr2.code,
|
|
353
|
+
errorClass: thrownErr2.name,
|
|
354
|
+
durationMs: Date.now() - start
|
|
355
|
+
});
|
|
356
|
+
throw thrownErr2;
|
|
357
|
+
}
|
|
358
|
+
const thrownErr = new RunwareApiError(
|
|
359
|
+
RUNWARE_ERROR_CODES.HTTP_FAILURE,
|
|
360
|
+
"Runware getBalance network error",
|
|
361
|
+
{ taskUUID }
|
|
362
|
+
);
|
|
363
|
+
cfg.logger.error("runware.get_balance.failed", {
|
|
364
|
+
taskUUID,
|
|
365
|
+
errorCode: thrownErr.code,
|
|
366
|
+
errorClass: thrownErr.name,
|
|
367
|
+
durationMs: Date.now() - start
|
|
368
|
+
});
|
|
369
|
+
throw thrownErr;
|
|
370
|
+
}
|
|
371
|
+
clearTimeout(timer);
|
|
372
|
+
if (!res.ok) {
|
|
373
|
+
const thrownErr = new RunwareApiError(
|
|
374
|
+
RUNWARE_ERROR_CODES.HTTP_FAILURE,
|
|
375
|
+
`Runware getBalance HTTP ${res.status}`,
|
|
376
|
+
{ status: res.status, taskUUID }
|
|
377
|
+
);
|
|
378
|
+
cfg.logger.error("runware.get_balance.failed", {
|
|
379
|
+
taskUUID,
|
|
380
|
+
errorCode: thrownErr.code,
|
|
381
|
+
errorClass: thrownErr.name,
|
|
382
|
+
durationMs: Date.now() - start
|
|
383
|
+
});
|
|
384
|
+
throw thrownErr;
|
|
385
|
+
}
|
|
386
|
+
const body = await res.json();
|
|
387
|
+
const balance = body.data?.[0]?.balance ?? 0;
|
|
388
|
+
cfg.logger.info("runware.get_balance.completed", {
|
|
389
|
+
taskUUID,
|
|
390
|
+
balance,
|
|
391
|
+
durationMs: Date.now() - start
|
|
392
|
+
});
|
|
393
|
+
return balance;
|
|
394
|
+
}
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// src/runware/prompt.ts
|
|
399
|
+
function assemblePrompts(input) {
|
|
400
|
+
const {
|
|
401
|
+
imagePrompt,
|
|
402
|
+
masterStylePrefix,
|
|
403
|
+
masterStyleSuffix,
|
|
404
|
+
characterPositive,
|
|
405
|
+
masterNegative,
|
|
406
|
+
characterNegative,
|
|
407
|
+
callNegative
|
|
408
|
+
} = input;
|
|
409
|
+
const positiveParts = [];
|
|
410
|
+
if (masterStylePrefix) positiveParts.push(masterStylePrefix);
|
|
411
|
+
if (characterPositive) positiveParts.push(characterPositive);
|
|
412
|
+
positiveParts.push(imagePrompt);
|
|
413
|
+
if (masterStyleSuffix) positiveParts.push(masterStyleSuffix);
|
|
414
|
+
const positive = positiveParts.join(" ");
|
|
415
|
+
const negativeParts = [];
|
|
416
|
+
if (masterNegative) negativeParts.push(masterNegative);
|
|
417
|
+
if (characterNegative) negativeParts.push(characterNegative);
|
|
418
|
+
if (callNegative) negativeParts.push(callNegative);
|
|
419
|
+
const negative = negativeParts.length > 0 ? negativeParts.join(", ") : void 0;
|
|
420
|
+
return { positive, negative };
|
|
421
|
+
}
|
|
422
|
+
export {
|
|
423
|
+
RUNWARE_ERROR_CODES,
|
|
424
|
+
RunwareApiError,
|
|
425
|
+
RunwareAuthError,
|
|
426
|
+
RunwareRateLimitError,
|
|
427
|
+
RunwareValidationError,
|
|
428
|
+
assemblePrompts,
|
|
429
|
+
createRunwareClient
|
|
430
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hazo_images",
|
|
3
|
-
"version": "1.1
|
|
3
|
+
"version": "1.2.1",
|
|
4
4
|
"description": "Image processing pipeline for the hazo ecosystem — Sharp wrapper, thumbnail generation, EXIF handling, and hazo_files integration helper",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -22,6 +22,11 @@
|
|
|
22
22
|
"types": "./dist/ui/index.d.ts",
|
|
23
23
|
"import": "./dist/ui/index.js",
|
|
24
24
|
"default": "./dist/ui/index.js"
|
|
25
|
+
},
|
|
26
|
+
"./runware": {
|
|
27
|
+
"types": "./dist/runware/index.d.ts",
|
|
28
|
+
"import": "./dist/runware/index.js",
|
|
29
|
+
"default": "./dist/runware/index.js"
|
|
25
30
|
}
|
|
26
31
|
},
|
|
27
32
|
"files": [
|
|
@@ -35,7 +40,9 @@
|
|
|
35
40
|
"build": "tsup",
|
|
36
41
|
"dev": "tsup --watch",
|
|
37
42
|
"typecheck": "tsc --noEmit",
|
|
38
|
-
"lint": "tsc --noEmit"
|
|
43
|
+
"lint": "tsc --noEmit",
|
|
44
|
+
"test": "node --experimental-vm-modules ../node_modules/jest/bin/jest.js --config jest.config.cjs",
|
|
45
|
+
"test:watch": "node --experimental-vm-modules ../node_modules/jest/bin/jest.js --config jest.config.cjs --watch"
|
|
39
46
|
},
|
|
40
47
|
"dependencies": {
|
|
41
48
|
"ini": "^4.1.0",
|
|
@@ -63,9 +70,13 @@
|
|
|
63
70
|
}
|
|
64
71
|
},
|
|
65
72
|
"devDependencies": {
|
|
73
|
+
"@types/jest": "^30.0.0",
|
|
66
74
|
"@types/node": "^22.10.0",
|
|
67
75
|
"@types/react": "^19.0.0",
|
|
68
|
-
"hazo_core": "^1.0.
|
|
76
|
+
"hazo_core": "^1.0.1",
|
|
77
|
+
"jest": "^30.2.0",
|
|
78
|
+
"jest-environment-node": "^30.2.0",
|
|
79
|
+
"ts-jest": "^29.4.5",
|
|
69
80
|
"tsup": "^8.0.0",
|
|
70
81
|
"typescript": "^5.7.2"
|
|
71
82
|
},
|