@nogataka/imgen 0.1.0 → 0.2.0

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/README.md CHANGED
@@ -136,6 +136,71 @@ imgen log -l error # エラーのみ
136
136
  3. 設定ファイル(`~/.imgen/config.json`)
137
137
  4. デフォルト値
138
138
 
139
+ ## SDK として使う
140
+
141
+ imgen は CLI だけでなく、Node.js ライブラリとしても利用できます。スライドジェネレーターや LLM ツールなど、他のアプリケーションから画像生成・編集・説明機能を呼び出せます。
142
+
143
+ ```bash
144
+ npm install @nogataka/imgen
145
+ ```
146
+
147
+ ```typescript
148
+ import {
149
+ AzureImageClient,
150
+ AzureChatClient,
151
+ getAzureConfig,
152
+ saveFileWithUniqueNameIfExists,
153
+ } from "@nogataka/imgen/sdk";
154
+
155
+ // Azure OpenAI 設定を取得(環境変数 or ~/.imgen/config.json)
156
+ const config = await getAzureConfig();
157
+
158
+ // 画像生成
159
+ const imageClient = new AzureImageClient(config);
160
+ const imageBytes = await imageClient.generateImage("夕日の海辺", {
161
+ size: "1536x1024",
162
+ quality: "high",
163
+ });
164
+ const savedPath = await saveFileWithUniqueNameIfExists("sunset.png", imageBytes);
165
+
166
+ // プロンプト拡張・画像説明
167
+ const chatClient = new AzureChatClient(config);
168
+ const prompt = await chatClient.generatePrompt("可愛い猫のマスコット");
169
+ const fileName = await chatClient.generateFileName("可愛い猫のマスコット");
170
+
171
+ // 画像編集
172
+ import * as fs from "node:fs/promises";
173
+ const photo = Buffer.from(await fs.readFile("photo.jpg"));
174
+ const edited = await imageClient.editImage(photo, "背景を青空に変更");
175
+
176
+ // 画像説明
177
+ import { readImageFile } from "@nogataka/imgen/sdk";
178
+ const imgData = await readImageFile("screenshot.png");
179
+ const explanation = await chatClient.generateExplanation(imgData, "ja");
180
+ ```
181
+
182
+ ### LLM ツールとしての組み込み例
183
+
184
+ ```typescript
185
+ import { AzureImageClient, AzureChatClient, getAzureConfig, saveFileWithUniqueNameIfExists } from "@nogataka/imgen/sdk";
186
+
187
+ const generateImageTool = {
188
+ name: "generate_image",
189
+ description: "テキストから画像を生成する",
190
+ execute: async ({ theme }: { theme: string }) => {
191
+ const config = await getAzureConfig();
192
+ const chat = new AzureChatClient(config);
193
+ const image = new AzureImageClient(config);
194
+
195
+ const prompt = await chat.generatePrompt(theme);
196
+ const fileName = await chat.generateFileName(theme);
197
+ const bytes = await image.generateImage(prompt, { size: "1024x1024", quality: "high" });
198
+ const path = await saveFileWithUniqueNameIfExists(`${fileName}.png`, bytes);
199
+ return { path };
200
+ },
201
+ };
202
+ ```
203
+
139
204
  ## 開発
140
205
 
141
206
  ```bash
@@ -0,0 +1,585 @@
1
+ // src/lang.ts
2
+ var LANGUAGE_DESCRIPTIONS = {
3
+ ja: "\u65E5\u672C\u8A9E",
4
+ en: "\u82F1\u8A9E",
5
+ zh: "\u4E2D\u56FD\u8A9E",
6
+ ko: "\u97D3\u56FD\u8A9E",
7
+ es: "\u30B9\u30DA\u30A4\u30F3\u8A9E",
8
+ fr: "\u30D5\u30E9\u30F3\u30B9\u8A9E",
9
+ de: "\u30C9\u30A4\u30C4\u8A9E",
10
+ it: "\u30A4\u30BF\u30EA\u30A2\u8A9E",
11
+ ru: "\u30ED\u30B7\u30A2\u8A9E",
12
+ vi: "\u30D9\u30C8\u30CA\u30E0\u8A9E"
13
+ };
14
+
15
+ // src/utils/config.ts
16
+ import * as fs from "fs/promises";
17
+ import * as os from "os";
18
+ import * as path from "path";
19
+ var DEFAULT_CONFIG = {
20
+ defaultLanguage: "ja",
21
+ defaultImageSize: "1024x1024",
22
+ defaultImageQuality: "high",
23
+ defaultImageFormat: "png",
24
+ logLevel: "info"
25
+ };
26
+ function getConfigDir() {
27
+ return path.join(os.homedir(), ".imgen");
28
+ }
29
+ function getConfigPath() {
30
+ return path.join(getConfigDir(), "config.json");
31
+ }
32
+ async function loadConfig() {
33
+ try {
34
+ const text = await fs.readFile(getConfigPath(), "utf-8");
35
+ return JSON.parse(text);
36
+ } catch {
37
+ return null;
38
+ }
39
+ }
40
+ async function saveConfig(config) {
41
+ const configPath = getConfigPath();
42
+ await fs.mkdir(path.dirname(configPath), { recursive: true });
43
+ await fs.writeFile(configPath, JSON.stringify(config, null, 2));
44
+ }
45
+ async function getAzureConfig() {
46
+ const config = await loadConfig();
47
+ const endpoint = process.env.AZURE_OPENAI_ENDPOINT || config?.azureEndpoint;
48
+ const apiKey = process.env.AZURE_OPENAI_API_KEY || config?.azureApiKey;
49
+ const deploymentName = process.env.AZURE_OPENAI_DEPLOYMENT_NAME || config?.azureDeploymentName;
50
+ const imageDeploymentName = process.env.AZURE_OPENAI_DEPLOYMENT_NAME_IMAGE || config?.azureImageDeploymentName;
51
+ const apiVersion = process.env.AZURE_OPENAI_API_VERSION || config?.azureApiVersion || "2024-02-15-preview";
52
+ const imageApiVersion = process.env.AZURE_OPENAI_IMAGE_API_VERSION || config?.azureImageApiVersion || "2025-04-01-preview";
53
+ if (!endpoint || !apiKey || !deploymentName || !imageDeploymentName) {
54
+ throw new Error(
55
+ "Azure OpenAI \u306E\u8A2D\u5B9A\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002`imgen configure` \u30B3\u30DE\u30F3\u30C9\u3067\u8A2D\u5B9A\u3059\u308B\u304B\u3001\u74B0\u5883\u5909\u6570\u3092\u8A2D\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044\u3002"
56
+ );
57
+ }
58
+ return { endpoint, apiKey, deploymentName, imageDeploymentName, apiVersion, imageApiVersion };
59
+ }
60
+
61
+ // src/utils/file.ts
62
+ import * as fs2 from "fs/promises";
63
+ async function generateUniqueFilePath(outputPath, maxRetries = 3) {
64
+ let finalPath = outputPath;
65
+ let retryCount = 0;
66
+ while (retryCount < maxRetries) {
67
+ try {
68
+ await fs2.stat(finalPath);
69
+ const baseName = finalPath.slice(0, finalPath.lastIndexOf("."));
70
+ const ext = finalPath.slice(finalPath.lastIndexOf("."));
71
+ const rand = Math.floor(Math.random() * 1e4).toString().padStart(4, "0");
72
+ finalPath = `${baseName}-${rand}${ext}`;
73
+ retryCount++;
74
+ } catch (error) {
75
+ if (error.code === "ENOENT") {
76
+ return finalPath;
77
+ }
78
+ throw error;
79
+ }
80
+ }
81
+ throw new Error(
82
+ `\u30D5\u30A1\u30A4\u30EB\u540D\u306E\u751F\u6210\u306B\u5931\u6557\u3057\u307E\u3057\u305F\u3002${maxRetries}\u56DE\u8A66\u884C\u3057\u307E\u3057\u305F\u304C\u3001\u3059\u3079\u3066\u65E2\u5B58\u306E\u30D5\u30A1\u30A4\u30EB\u540D\u3068\u885D\u7A81\u3057\u3066\u3044\u307E\u3059\u3002`
83
+ );
84
+ }
85
+ async function saveFileWithUniqueNameIfExists(outputPath, data, maxRetries = 3) {
86
+ const finalPath = await generateUniqueFilePath(outputPath, maxRetries);
87
+ await fs2.writeFile(finalPath, data);
88
+ return finalPath;
89
+ }
90
+ async function loadContextFile(contextPath) {
91
+ if (!contextPath) return "";
92
+ try {
93
+ return await fs2.readFile(contextPath, "utf-8");
94
+ } catch (error) {
95
+ if (error.code === "ENOENT") {
96
+ throw new Error(`\u30B3\u30F3\u30C6\u30AD\u30B9\u30C8\u30D5\u30A1\u30A4\u30EB\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093: ${contextPath}`);
97
+ }
98
+ if (error instanceof Error) {
99
+ throw new Error(`\u30B3\u30F3\u30C6\u30AD\u30B9\u30C8\u30D5\u30A1\u30A4\u30EB\u306E\u8AAD\u307F\u8FBC\u307F\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${error.message}`);
100
+ }
101
+ throw new Error(`\u30B3\u30F3\u30C6\u30AD\u30B9\u30C8\u30D5\u30A1\u30A4\u30EB\u306E\u8AAD\u307F\u8FBC\u307F\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${String(error)}`);
102
+ }
103
+ }
104
+ async function fileExists(filePath) {
105
+ try {
106
+ await fs2.access(filePath);
107
+ return true;
108
+ } catch {
109
+ return false;
110
+ }
111
+ }
112
+
113
+ // src/utils/azure-chat.ts
114
+ import { AzureOpenAI } from "openai";
115
+
116
+ // src/utils/logger.ts
117
+ import * as fs3 from "fs/promises";
118
+ import * as path2 from "path";
119
+ import * as os2 from "os";
120
+ import { format } from "date-fns";
121
+ var Logger = class _Logger {
122
+ constructor(name) {
123
+ this.name = name;
124
+ const home = os2.homedir();
125
+ this.logDir = path2.join(home, ".imgen", "logs");
126
+ this.currentLogFile = this.generateLogFileName();
127
+ }
128
+ static instances = /* @__PURE__ */ new Map();
129
+ static globalConfig = {
130
+ destination: "CONSOLE" /* CONSOLE */,
131
+ minLevel: "INFO" /* INFO */
132
+ };
133
+ static currentContext = "default";
134
+ logDir;
135
+ currentLogFile;
136
+ static setGlobalConfig(config) {
137
+ _Logger.globalConfig = { ..._Logger.globalConfig, ...config };
138
+ }
139
+ static setContext(name) {
140
+ _Logger.currentContext = name;
141
+ }
142
+ static getInstance(options) {
143
+ const { name } = options;
144
+ if (!_Logger.instances.has(name)) {
145
+ _Logger.instances.set(name, new _Logger(name));
146
+ }
147
+ return _Logger.instances.get(name);
148
+ }
149
+ generateLogFileName() {
150
+ return path2.join(this.logDir, `${this.name}-${format(/* @__PURE__ */ new Date(), "yyyy-MM-dd")}.log`);
151
+ }
152
+ async ensureLogDirectory() {
153
+ await fs3.mkdir(this.logDir, { recursive: true });
154
+ }
155
+ formatLogEntry(level, message, data) {
156
+ return { timestamp: (/* @__PURE__ */ new Date()).toISOString(), level, message, data };
157
+ }
158
+ shouldLog(level) {
159
+ const priority = {
160
+ ["DEBUG" /* DEBUG */]: 0,
161
+ ["INFO" /* INFO */]: 1,
162
+ ["WARN" /* WARN */]: 2,
163
+ ["ERROR" /* ERROR */]: 3
164
+ };
165
+ return priority[level] >= priority[_Logger.globalConfig.minLevel];
166
+ }
167
+ async writeLog(entry) {
168
+ if (!this.shouldLog(entry.level)) return;
169
+ const { destination } = _Logger.globalConfig;
170
+ if (destination === "CONSOLE" /* CONSOLE */ || destination === "BOTH" /* BOTH */) {
171
+ this.writeToConsole(entry);
172
+ }
173
+ if (destination === "FILE" /* FILE */ || destination === "BOTH" /* BOTH */) {
174
+ await this.writeToFile(entry);
175
+ }
176
+ }
177
+ writeToConsole(entry) {
178
+ const ts = entry.timestamp.replace("T", " ").replace(/\.\d+Z$/, "");
179
+ const dataStr = entry.data ? ` ${JSON.stringify(entry.data)}` : "";
180
+ const msg = `[${ts}] [${this.name}] [${entry.level}] ${entry.message}${dataStr}`;
181
+ switch (entry.level) {
182
+ case "DEBUG" /* DEBUG */:
183
+ console.debug(msg);
184
+ break;
185
+ case "INFO" /* INFO */:
186
+ console.info(msg);
187
+ break;
188
+ case "WARN" /* WARN */:
189
+ console.warn(msg);
190
+ break;
191
+ case "ERROR" /* ERROR */:
192
+ console.error(msg);
193
+ break;
194
+ }
195
+ }
196
+ async writeToFile(entry) {
197
+ await this.ensureLogDirectory();
198
+ try {
199
+ await fs3.appendFile(this.currentLogFile, JSON.stringify(entry) + "\n");
200
+ } catch (error) {
201
+ console.error(
202
+ `\u30ED\u30B0\u306E\u66F8\u304D\u8FBC\u307F\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${error instanceof Error ? error.message : String(error)}`
203
+ );
204
+ }
205
+ }
206
+ debug(message, data) {
207
+ return this.writeLog(this.formatLogEntry("DEBUG" /* DEBUG */, message, data));
208
+ }
209
+ info(message, data) {
210
+ return this.writeLog(this.formatLogEntry("INFO" /* INFO */, message, data));
211
+ }
212
+ warn(message, data) {
213
+ return this.writeLog(this.formatLogEntry("WARN" /* WARN */, message, data));
214
+ }
215
+ error(message, data) {
216
+ return this.writeLog(this.formatLogEntry("ERROR" /* ERROR */, message, data));
217
+ }
218
+ static debug(message, data) {
219
+ return _Logger.getInstance({ name: _Logger.currentContext }).debug(message, data);
220
+ }
221
+ static info(message, data) {
222
+ return _Logger.getInstance({ name: _Logger.currentContext }).info(message, data);
223
+ }
224
+ static warn(message, data) {
225
+ return _Logger.getInstance({ name: _Logger.currentContext }).warn(message, data);
226
+ }
227
+ static error(message, data) {
228
+ return _Logger.getInstance({ name: _Logger.currentContext }).error(message, data);
229
+ }
230
+ getLatestLogFilePath() {
231
+ return this.currentLogFile;
232
+ }
233
+ async getLogEntries(minLevel = "INFO" /* INFO */, maxEntries = 100) {
234
+ try {
235
+ await this.ensureLogDirectory();
236
+ const content = await fs3.readFile(this.currentLogFile, "utf-8");
237
+ const lines = content.trim().split("\n");
238
+ const priority = {
239
+ ["DEBUG" /* DEBUG */]: 0,
240
+ ["INFO" /* INFO */]: 1,
241
+ ["WARN" /* WARN */]: 2,
242
+ ["ERROR" /* ERROR */]: 3
243
+ };
244
+ const entries = [];
245
+ for (let i = lines.length - 1; i >= 0 && entries.length < maxEntries; i--) {
246
+ try {
247
+ const entry = JSON.parse(lines[i]);
248
+ if (priority[entry.level] >= priority[minLevel]) entries.unshift(entry);
249
+ } catch {
250
+ continue;
251
+ }
252
+ }
253
+ return entries;
254
+ } catch (error) {
255
+ if (error.code === "ENOENT") return [];
256
+ throw error;
257
+ }
258
+ }
259
+ };
260
+
261
+ // src/utils/azure-chat.ts
262
+ var AzureChatClient = class {
263
+ client;
264
+ deploymentName;
265
+ logger;
266
+ constructor(config) {
267
+ this.client = new AzureOpenAI({
268
+ endpoint: config.endpoint,
269
+ apiKey: config.apiKey,
270
+ apiVersion: config.apiVersion,
271
+ deployment: config.deploymentName
272
+ });
273
+ this.deploymentName = config.deploymentName;
274
+ this.logger = Logger.getInstance({ name: "azure-chat" });
275
+ }
276
+ /**
277
+ * Generates a detailed image-generation prompt from a short theme description.
278
+ * Optionally accepts additional context to guide prompt generation.
279
+ */
280
+ async generatePrompt(theme, context = "") {
281
+ if (!theme) throw new Error("\u30C6\u30FC\u30DE\u304C\u7A7A\u3067\u3059");
282
+ const prompt = `
283
+ Generate a detailed image generation prompt based on the following information.
284
+
285
+ Theme: ${theme}
286
+ ${context ? `Context:
287
+ ${context}
288
+ ` : ""}
289
+ Please generate a prompt that meets the following criteria:
290
+ 1. Include specific and detailed descriptions
291
+ 2. Clearly specify the image style and atmosphere
292
+ 3. Include all necessary elements
293
+ 4. Output in English
294
+ 5. Focus on visual elements and composition
295
+ 6. Include lighting and color descriptions
296
+ 7. Specify the mood and emotional tone
297
+ 8. Limit the output to approximately 1500 characters
298
+
299
+ Prompt:
300
+ `;
301
+ try {
302
+ const response = await this.client.chat.completions.create({
303
+ model: this.deploymentName,
304
+ messages: [{ role: "user", content: prompt }]
305
+ });
306
+ return response.choices[0]?.message?.content ?? "";
307
+ } catch (error) {
308
+ this.logger.error("\u30D7\u30ED\u30F3\u30D7\u30C8\u751F\u6210\u306B\u5931\u6557\u3057\u307E\u3057\u305F", { error });
309
+ throw new Error("\u30D7\u30ED\u30F3\u30D7\u30C8\u306E\u751F\u6210\u306B\u5931\u6557\u3057\u307E\u3057\u305F");
310
+ }
311
+ }
312
+ /**
313
+ * Generates a sanitized file name (lowercase alphanumeric + hyphens only) from a theme.
314
+ */
315
+ async generateFileName(theme, maxLength = 40) {
316
+ if (!theme) throw new Error("\u30C6\u30FC\u30DE\u304C\u7A7A\u3067\u3059");
317
+ try {
318
+ const response = await this.client.chat.completions.create({
319
+ model: this.deploymentName,
320
+ messages: [
321
+ {
322
+ role: "user",
323
+ content: `\u4EE5\u4E0B\u306E\u30C6\u30FC\u30DE\u304B\u3089\u753B\u50CF\u306E\u30D5\u30A1\u30A4\u30EB\u540D\u3092\u751F\u6210\u3057\u3066\u304F\u3060\u3055\u3044\u3002\u82F1\u5C0F\u6587\u5B57\u3068\u30CF\u30A4\u30D5\u30F3\u306E\u307F\u3001${maxLength}\u6587\u5B57\u4EE5\u5185\u3002\u62E1\u5F35\u5B50\u306A\u3057\u3002
324
+
325
+ \u30C6\u30FC\u30DE: ${theme}
326
+
327
+ \u30D5\u30A1\u30A4\u30EB\u540D:`
328
+ }
329
+ ]
330
+ });
331
+ let fileName = (response.choices[0]?.message?.content ?? "").trim();
332
+ fileName = fileName.toLowerCase().replace(/[^a-z0-9-]/g, "").replace(/-+/g, "-").replace(/^-|-$/g, "");
333
+ if (fileName.length > maxLength) fileName = fileName.substring(0, maxLength);
334
+ return fileName || "image";
335
+ } catch (error) {
336
+ this.logger.error("\u30D5\u30A1\u30A4\u30EB\u540D\u751F\u6210\u306B\u5931\u6557\u3057\u307E\u3057\u305F", { error });
337
+ throw new Error("\u30D5\u30A1\u30A4\u30EB\u540D\u306E\u751F\u6210\u306B\u5931\u6557\u3057\u307E\u3057\u305F");
338
+ }
339
+ }
340
+ /**
341
+ * Generates a detailed explanation of an image using multimodal (vision) input.
342
+ * The explanation language is controlled by the `lang` parameter.
343
+ */
344
+ async generateExplanation(imageData, lang = "ja", context) {
345
+ try {
346
+ const response = await this.client.chat.completions.create({
347
+ model: this.deploymentName,
348
+ messages: [
349
+ {
350
+ role: "user",
351
+ content: [
352
+ {
353
+ type: "image_url",
354
+ image_url: { url: `data:${imageData.mimeType};base64,${imageData.data}` }
355
+ },
356
+ {
357
+ type: "text",
358
+ text: `\u3053\u306E\u753B\u50CF\u306B\u3064\u3044\u3066\u3001${lang}\u3067\u8A73\u7D30\u306A\u8AAC\u660E\u3092\u751F\u6210\u3057\u3066\u304F\u3060\u3055\u3044\u3002${context ? `
359
+
360
+ \u30B3\u30F3\u30C6\u30AD\u30B9\u30C8\u60C5\u5831:
361
+ ${context}` : ""}`
362
+ }
363
+ ]
364
+ }
365
+ ]
366
+ });
367
+ return response.choices[0]?.message?.content ?? "";
368
+ } catch (error) {
369
+ this.logger.error("\u753B\u50CF\u8AAC\u660E\u306E\u751F\u6210\u306B\u5931\u6557\u3057\u307E\u3057\u305F", { error });
370
+ throw new Error("\u753B\u50CF\u306E\u8AAC\u660E\u751F\u6210\u306B\u5931\u6557\u3057\u307E\u3057\u305F");
371
+ }
372
+ }
373
+ };
374
+
375
+ // src/utils/azure-image.ts
376
+ import { AzureOpenAI as AzureOpenAI2 } from "openai";
377
+ var AzureImageClient = class {
378
+ client;
379
+ config;
380
+ logger;
381
+ constructor(config) {
382
+ this.config = config;
383
+ this.client = new AzureOpenAI2({
384
+ endpoint: config.endpoint,
385
+ apiKey: config.apiKey,
386
+ apiVersion: config.imageApiVersion,
387
+ deployment: config.imageDeploymentName
388
+ });
389
+ this.logger = Logger.getInstance({ name: "azure-image" });
390
+ }
391
+ /**
392
+ * Generates an image from a text prompt using the Azure OpenAI SDK.
393
+ * Returns raw image bytes as a Uint8Array.
394
+ */
395
+ async generateImage(prompt, options) {
396
+ const { size = "1024x1024", quality = "high" } = options;
397
+ this.logger.debug("\u753B\u50CF\u751F\u6210\u30EA\u30AF\u30A8\u30B9\u30C8", { prompt: prompt.substring(0, 100), size, quality });
398
+ try {
399
+ const response = await this.client.images.generate({
400
+ model: this.config.imageDeploymentName,
401
+ prompt,
402
+ n: 1,
403
+ size,
404
+ quality,
405
+ output_format: "png"
406
+ });
407
+ if (!response.data || response.data.length === 0 || !response.data[0].b64_json) {
408
+ throw new Error("\u753B\u50CF\u30C7\u30FC\u30BF\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093");
409
+ }
410
+ const b64 = response.data[0].b64_json;
411
+ const binary = atob(b64);
412
+ const bytes = new Uint8Array(binary.length);
413
+ for (let i = 0; i < binary.length; i++) {
414
+ bytes[i] = binary.charCodeAt(i);
415
+ }
416
+ return bytes;
417
+ } catch (error) {
418
+ if (error instanceof Error && error.message === "\u753B\u50CF\u30C7\u30FC\u30BF\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093") throw error;
419
+ this.logger.error("\u753B\u50CF\u751F\u6210\u306B\u5931\u6557\u3057\u307E\u3057\u305F", { error });
420
+ throw new Error(
421
+ `\u753B\u50CF\u751F\u6210\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${error instanceof Error ? error.message : String(error)}`
422
+ );
423
+ }
424
+ }
425
+ /**
426
+ * Edits an existing image using the Azure OpenAI REST API.
427
+ * Uses fetch + FormData because the SDK's image editing support is unreliable.
428
+ * Returns raw image bytes as a Uint8Array.
429
+ */
430
+ async editImage(imageBuffer, prompt, options = {}) {
431
+ const { size = "1024x1024" } = options;
432
+ this.logger.debug("\u753B\u50CF\u7DE8\u96C6\u30EA\u30AF\u30A8\u30B9\u30C8 (REST API)", { prompt: prompt.substring(0, 100), size });
433
+ const url = `${this.config.endpoint}/openai/deployments/${this.config.imageDeploymentName}/images/edits?api-version=${this.config.imageApiVersion}`;
434
+ const blob = new Blob([imageBuffer], { type: "image/png" });
435
+ const formData = new FormData();
436
+ formData.append("image", blob, "image.png");
437
+ formData.append("prompt", prompt);
438
+ formData.append("size", size);
439
+ try {
440
+ const response = await fetch(url, {
441
+ method: "POST",
442
+ headers: { "api-key": this.config.apiKey },
443
+ body: formData
444
+ });
445
+ if (!response.ok) {
446
+ const errorText = await response.text();
447
+ throw new Error(`Azure API error (${response.status}): ${errorText}`);
448
+ }
449
+ const json = await response.json();
450
+ if (!json.data || json.data.length === 0 || !json.data[0].b64_json) {
451
+ throw new Error("\u753B\u50CF\u30C7\u30FC\u30BF\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093");
452
+ }
453
+ const b64 = json.data[0].b64_json;
454
+ const binary = atob(b64);
455
+ const bytes = new Uint8Array(binary.length);
456
+ for (let i = 0; i < binary.length; i++) {
457
+ bytes[i] = binary.charCodeAt(i);
458
+ }
459
+ return bytes;
460
+ } catch (error) {
461
+ if (error instanceof Error && error.message === "\u753B\u50CF\u30C7\u30FC\u30BF\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093") throw error;
462
+ this.logger.error("\u753B\u50CF\u7DE8\u96C6\u306B\u5931\u6557\u3057\u307E\u3057\u305F", { error });
463
+ throw new Error(
464
+ `\u753B\u50CF\u7DE8\u96C6\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${error instanceof Error ? error.message : String(error)}`
465
+ );
466
+ }
467
+ }
468
+ };
469
+
470
+ // src/utils/image.ts
471
+ import * as fs4 from "fs/promises";
472
+ async function readImageFile(filePath) {
473
+ try {
474
+ const buffer = await fs4.readFile(filePath);
475
+ return {
476
+ data: buffer.toString("base64"),
477
+ mimeType: getMimeType(filePath)
478
+ };
479
+ } catch (error) {
480
+ if (error instanceof Error && error.message.startsWith("\u30B5\u30DD\u30FC\u30C8\u3055\u308C\u3066\u3044\u306A\u3044")) {
481
+ throw error;
482
+ }
483
+ throw new Error(
484
+ `\u753B\u50CF\u30D5\u30A1\u30A4\u30EB\u306E\u8AAD\u307F\u8FBC\u307F\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${error instanceof Error ? error.message : String(error)}`
485
+ );
486
+ }
487
+ }
488
+ function getMimeType(filePath) {
489
+ const ext = filePath.toLowerCase().split(".").pop();
490
+ const map = {
491
+ jpg: "image/jpeg",
492
+ jpeg: "image/jpeg",
493
+ png: "image/png",
494
+ gif: "image/gif",
495
+ webp: "image/webp"
496
+ };
497
+ if (ext && map[ext]) return map[ext];
498
+ throw new Error(`\u30B5\u30DD\u30FC\u30C8\u3055\u308C\u3066\u3044\u306A\u3044\u30D5\u30A1\u30A4\u30EB\u5F62\u5F0F\u3067\u3059: .${ext}`);
499
+ }
500
+
501
+ // src/utils/preset.ts
502
+ import * as fs5 from "fs/promises";
503
+ import * as path3 from "path";
504
+ var BUILTIN_PRESETS = {
505
+ "builtin:square": { size: "1024x1024", quality: "high" },
506
+ "builtin:landscape": { size: "1536x1024", quality: "high" },
507
+ "builtin:portrait": { size: "1024x1536", quality: "high" },
508
+ "builtin:draft": { size: "1024x1024", quality: "low" },
509
+ "builtin:photo": { size: "1536x1024", quality: "high" }
510
+ };
511
+ function getPresetsPath() {
512
+ return path3.join(getConfigDir(), "presets.json");
513
+ }
514
+ async function loadPresets() {
515
+ try {
516
+ const text = await fs5.readFile(getPresetsPath(), "utf-8");
517
+ return JSON.parse(text);
518
+ } catch {
519
+ return {};
520
+ }
521
+ }
522
+ async function savePresets(presets) {
523
+ const p = getPresetsPath();
524
+ await fs5.mkdir(path3.dirname(p), { recursive: true });
525
+ await fs5.writeFile(p, JSON.stringify(presets, null, 2));
526
+ }
527
+ async function getPreset(name) {
528
+ if (name.startsWith("builtin:") && BUILTIN_PRESETS[name]) {
529
+ return BUILTIN_PRESETS[name];
530
+ }
531
+ const presets = await loadPresets();
532
+ return presets[name] || null;
533
+ }
534
+ async function savePreset(name, preset) {
535
+ if (name.startsWith("builtin:")) {
536
+ throw new Error("\u30D3\u30EB\u30C8\u30A4\u30F3\u30D7\u30EA\u30BB\u30C3\u30C8\u306F\u4E0A\u66F8\u304D\u3067\u304D\u307E\u305B\u3093");
537
+ }
538
+ const presets = await loadPresets();
539
+ presets[name] = preset;
540
+ await savePresets(presets);
541
+ }
542
+ async function deletePreset(name) {
543
+ if (name.startsWith("builtin:")) {
544
+ throw new Error("\u30D3\u30EB\u30C8\u30A4\u30F3\u30D7\u30EA\u30BB\u30C3\u30C8\u306F\u524A\u9664\u3067\u304D\u307E\u305B\u3093");
545
+ }
546
+ const presets = await loadPresets();
547
+ if (!presets[name]) return false;
548
+ delete presets[name];
549
+ await savePresets(presets);
550
+ return true;
551
+ }
552
+ async function listAllPresets() {
553
+ const result = [];
554
+ for (const [name, preset] of Object.entries(BUILTIN_PRESETS)) {
555
+ result.push({ name, preset, builtin: true });
556
+ }
557
+ const presets = await loadPresets();
558
+ for (const [name, preset] of Object.entries(presets)) {
559
+ result.push({ name, preset, builtin: false });
560
+ }
561
+ return result;
562
+ }
563
+
564
+ export {
565
+ LANGUAGE_DESCRIPTIONS,
566
+ DEFAULT_CONFIG,
567
+ getConfigPath,
568
+ loadConfig,
569
+ saveConfig,
570
+ getAzureConfig,
571
+ saveFileWithUniqueNameIfExists,
572
+ loadContextFile,
573
+ fileExists,
574
+ Logger,
575
+ AzureChatClient,
576
+ AzureImageClient,
577
+ readImageFile,
578
+ getMimeType,
579
+ BUILTIN_PRESETS,
580
+ getPresetsPath,
581
+ getPreset,
582
+ savePreset,
583
+ deletePreset,
584
+ listAllPresets
585
+ };