@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 +65 -0
- package/dist/chunk-HQT7ZTCY.js +585 -0
- package/dist/index.js +45 -594
- package/dist/sdk.d.ts +154 -0
- package/dist/sdk.js +32 -0
- package/package.json +6 -3
package/dist/index.js
CHANGED
|
@@ -1,4 +1,24 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
AzureChatClient,
|
|
4
|
+
AzureImageClient,
|
|
5
|
+
DEFAULT_CONFIG,
|
|
6
|
+
LANGUAGE_DESCRIPTIONS,
|
|
7
|
+
Logger,
|
|
8
|
+
deletePreset,
|
|
9
|
+
fileExists,
|
|
10
|
+
getAzureConfig,
|
|
11
|
+
getConfigPath,
|
|
12
|
+
getPreset,
|
|
13
|
+
getPresetsPath,
|
|
14
|
+
listAllPresets,
|
|
15
|
+
loadConfig,
|
|
16
|
+
loadContextFile,
|
|
17
|
+
readImageFile,
|
|
18
|
+
saveConfig,
|
|
19
|
+
saveFileWithUniqueNameIfExists,
|
|
20
|
+
savePreset
|
|
21
|
+
} from "./chunk-HQT7ZTCY.js";
|
|
2
22
|
|
|
3
23
|
// src/commands/index.ts
|
|
4
24
|
import { Command as Command8 } from "commander";
|
|
@@ -6,68 +26,6 @@ import { Command as Command8 } from "commander";
|
|
|
6
26
|
// src/commands/configure.ts
|
|
7
27
|
import { Command } from "commander";
|
|
8
28
|
import inquirer from "inquirer";
|
|
9
|
-
|
|
10
|
-
// src/lang.ts
|
|
11
|
-
var LANGUAGE_DESCRIPTIONS = {
|
|
12
|
-
ja: "\u65E5\u672C\u8A9E",
|
|
13
|
-
en: "\u82F1\u8A9E",
|
|
14
|
-
zh: "\u4E2D\u56FD\u8A9E",
|
|
15
|
-
ko: "\u97D3\u56FD\u8A9E",
|
|
16
|
-
es: "\u30B9\u30DA\u30A4\u30F3\u8A9E",
|
|
17
|
-
fr: "\u30D5\u30E9\u30F3\u30B9\u8A9E",
|
|
18
|
-
de: "\u30C9\u30A4\u30C4\u8A9E",
|
|
19
|
-
it: "\u30A4\u30BF\u30EA\u30A2\u8A9E",
|
|
20
|
-
ru: "\u30ED\u30B7\u30A2\u8A9E",
|
|
21
|
-
vi: "\u30D9\u30C8\u30CA\u30E0\u8A9E"
|
|
22
|
-
};
|
|
23
|
-
|
|
24
|
-
// src/utils/config.ts
|
|
25
|
-
import * as fs from "fs/promises";
|
|
26
|
-
import * as os from "os";
|
|
27
|
-
import * as path from "path";
|
|
28
|
-
var DEFAULT_CONFIG = {
|
|
29
|
-
defaultLanguage: "ja",
|
|
30
|
-
defaultImageSize: "1024x1024",
|
|
31
|
-
defaultImageQuality: "high",
|
|
32
|
-
defaultImageFormat: "png",
|
|
33
|
-
logLevel: "info"
|
|
34
|
-
};
|
|
35
|
-
function getConfigDir() {
|
|
36
|
-
return path.join(os.homedir(), ".imgen");
|
|
37
|
-
}
|
|
38
|
-
function getConfigPath() {
|
|
39
|
-
return path.join(getConfigDir(), "config.json");
|
|
40
|
-
}
|
|
41
|
-
async function loadConfig() {
|
|
42
|
-
try {
|
|
43
|
-
const text = await fs.readFile(getConfigPath(), "utf-8");
|
|
44
|
-
return JSON.parse(text);
|
|
45
|
-
} catch {
|
|
46
|
-
return null;
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
async function saveConfig(config) {
|
|
50
|
-
const configPath = getConfigPath();
|
|
51
|
-
await fs.mkdir(path.dirname(configPath), { recursive: true });
|
|
52
|
-
await fs.writeFile(configPath, JSON.stringify(config, null, 2));
|
|
53
|
-
}
|
|
54
|
-
async function getAzureConfig() {
|
|
55
|
-
const config = await loadConfig();
|
|
56
|
-
const endpoint = process.env.AZURE_OPENAI_ENDPOINT || config?.azureEndpoint;
|
|
57
|
-
const apiKey = process.env.AZURE_OPENAI_API_KEY || config?.azureApiKey;
|
|
58
|
-
const deploymentName = process.env.AZURE_OPENAI_DEPLOYMENT_NAME || config?.azureDeploymentName;
|
|
59
|
-
const imageDeploymentName = process.env.AZURE_OPENAI_DEPLOYMENT_NAME_IMAGE || config?.azureImageDeploymentName;
|
|
60
|
-
const apiVersion = process.env.AZURE_OPENAI_API_VERSION || config?.azureApiVersion || "2024-02-15-preview";
|
|
61
|
-
const imageApiVersion = process.env.AZURE_OPENAI_IMAGE_API_VERSION || config?.azureImageApiVersion || "2025-04-01-preview";
|
|
62
|
-
if (!endpoint || !apiKey || !deploymentName || !imageDeploymentName) {
|
|
63
|
-
throw new Error(
|
|
64
|
-
"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"
|
|
65
|
-
);
|
|
66
|
-
}
|
|
67
|
-
return { endpoint, apiKey, deploymentName, imageDeploymentName, apiVersion, imageApiVersion };
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
// src/commands/configure.ts
|
|
71
29
|
function configureCommand() {
|
|
72
30
|
return new Command("configure").description("API\u8A2D\u5B9A\u3092\u884C\u3044\u307E\u3059").option("--show", "\u73FE\u5728\u306E\u8A2D\u5B9A\u3092\u8868\u793A").option("--reset", "\u8A2D\u5B9A\u3092\u30EA\u30BB\u30C3\u30C8").action(async (options) => {
|
|
73
31
|
try {
|
|
@@ -291,419 +249,10 @@ async function configureDefaults() {
|
|
|
291
249
|
import { Command as Command5 } from "commander";
|
|
292
250
|
|
|
293
251
|
// src/commands/image/edit.ts
|
|
294
|
-
import * as
|
|
295
|
-
import * as
|
|
252
|
+
import * as fs from "fs/promises";
|
|
253
|
+
import * as path from "path";
|
|
296
254
|
import { Command as Command2, Option } from "commander";
|
|
297
255
|
|
|
298
|
-
// src/utils/file.ts
|
|
299
|
-
import * as fs2 from "fs/promises";
|
|
300
|
-
async function generateUniqueFilePath(outputPath, maxRetries = 3) {
|
|
301
|
-
let finalPath = outputPath;
|
|
302
|
-
let retryCount = 0;
|
|
303
|
-
while (retryCount < maxRetries) {
|
|
304
|
-
try {
|
|
305
|
-
await fs2.stat(finalPath);
|
|
306
|
-
const baseName = finalPath.slice(0, finalPath.lastIndexOf("."));
|
|
307
|
-
const ext = finalPath.slice(finalPath.lastIndexOf("."));
|
|
308
|
-
const rand = Math.floor(Math.random() * 1e4).toString().padStart(4, "0");
|
|
309
|
-
finalPath = `${baseName}-${rand}${ext}`;
|
|
310
|
-
retryCount++;
|
|
311
|
-
} catch (error) {
|
|
312
|
-
if (error.code === "ENOENT") {
|
|
313
|
-
return finalPath;
|
|
314
|
-
}
|
|
315
|
-
throw error;
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
throw new Error(
|
|
319
|
-
`\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`
|
|
320
|
-
);
|
|
321
|
-
}
|
|
322
|
-
async function saveFileWithUniqueNameIfExists(outputPath, data, maxRetries = 3) {
|
|
323
|
-
const finalPath = await generateUniqueFilePath(outputPath, maxRetries);
|
|
324
|
-
await fs2.writeFile(finalPath, data);
|
|
325
|
-
return finalPath;
|
|
326
|
-
}
|
|
327
|
-
async function loadContextFile(contextPath) {
|
|
328
|
-
if (!contextPath) return "";
|
|
329
|
-
try {
|
|
330
|
-
return await fs2.readFile(contextPath, "utf-8");
|
|
331
|
-
} catch (error) {
|
|
332
|
-
if (error.code === "ENOENT") {
|
|
333
|
-
throw new Error(`\u30B3\u30F3\u30C6\u30AD\u30B9\u30C8\u30D5\u30A1\u30A4\u30EB\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093: ${contextPath}`);
|
|
334
|
-
}
|
|
335
|
-
if (error instanceof Error) {
|
|
336
|
-
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}`);
|
|
337
|
-
}
|
|
338
|
-
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)}`);
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
async function fileExists(filePath) {
|
|
342
|
-
try {
|
|
343
|
-
await fs2.access(filePath);
|
|
344
|
-
return true;
|
|
345
|
-
} catch {
|
|
346
|
-
return false;
|
|
347
|
-
}
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
// src/utils/azure-chat.ts
|
|
351
|
-
import { AzureOpenAI } from "openai";
|
|
352
|
-
|
|
353
|
-
// src/utils/logger.ts
|
|
354
|
-
import * as fs3 from "fs/promises";
|
|
355
|
-
import * as path2 from "path";
|
|
356
|
-
import * as os2 from "os";
|
|
357
|
-
import { format } from "date-fns";
|
|
358
|
-
var Logger = class _Logger {
|
|
359
|
-
constructor(name) {
|
|
360
|
-
this.name = name;
|
|
361
|
-
const home = os2.homedir();
|
|
362
|
-
this.logDir = path2.join(home, ".imgen", "logs");
|
|
363
|
-
this.currentLogFile = this.generateLogFileName();
|
|
364
|
-
}
|
|
365
|
-
static instances = /* @__PURE__ */ new Map();
|
|
366
|
-
static globalConfig = {
|
|
367
|
-
destination: "CONSOLE" /* CONSOLE */,
|
|
368
|
-
minLevel: "INFO" /* INFO */
|
|
369
|
-
};
|
|
370
|
-
static currentContext = "default";
|
|
371
|
-
logDir;
|
|
372
|
-
currentLogFile;
|
|
373
|
-
static setGlobalConfig(config) {
|
|
374
|
-
_Logger.globalConfig = { ..._Logger.globalConfig, ...config };
|
|
375
|
-
}
|
|
376
|
-
static setContext(name) {
|
|
377
|
-
_Logger.currentContext = name;
|
|
378
|
-
}
|
|
379
|
-
static getInstance(options) {
|
|
380
|
-
const { name } = options;
|
|
381
|
-
if (!_Logger.instances.has(name)) {
|
|
382
|
-
_Logger.instances.set(name, new _Logger(name));
|
|
383
|
-
}
|
|
384
|
-
return _Logger.instances.get(name);
|
|
385
|
-
}
|
|
386
|
-
generateLogFileName() {
|
|
387
|
-
return path2.join(this.logDir, `${this.name}-${format(/* @__PURE__ */ new Date(), "yyyy-MM-dd")}.log`);
|
|
388
|
-
}
|
|
389
|
-
async ensureLogDirectory() {
|
|
390
|
-
await fs3.mkdir(this.logDir, { recursive: true });
|
|
391
|
-
}
|
|
392
|
-
formatLogEntry(level, message, data) {
|
|
393
|
-
return { timestamp: (/* @__PURE__ */ new Date()).toISOString(), level, message, data };
|
|
394
|
-
}
|
|
395
|
-
shouldLog(level) {
|
|
396
|
-
const priority = {
|
|
397
|
-
["DEBUG" /* DEBUG */]: 0,
|
|
398
|
-
["INFO" /* INFO */]: 1,
|
|
399
|
-
["WARN" /* WARN */]: 2,
|
|
400
|
-
["ERROR" /* ERROR */]: 3
|
|
401
|
-
};
|
|
402
|
-
return priority[level] >= priority[_Logger.globalConfig.minLevel];
|
|
403
|
-
}
|
|
404
|
-
async writeLog(entry) {
|
|
405
|
-
if (!this.shouldLog(entry.level)) return;
|
|
406
|
-
const { destination } = _Logger.globalConfig;
|
|
407
|
-
if (destination === "CONSOLE" /* CONSOLE */ || destination === "BOTH" /* BOTH */) {
|
|
408
|
-
this.writeToConsole(entry);
|
|
409
|
-
}
|
|
410
|
-
if (destination === "FILE" /* FILE */ || destination === "BOTH" /* BOTH */) {
|
|
411
|
-
await this.writeToFile(entry);
|
|
412
|
-
}
|
|
413
|
-
}
|
|
414
|
-
writeToConsole(entry) {
|
|
415
|
-
const ts = entry.timestamp.replace("T", " ").replace(/\.\d+Z$/, "");
|
|
416
|
-
const dataStr = entry.data ? ` ${JSON.stringify(entry.data)}` : "";
|
|
417
|
-
const msg = `[${ts}] [${this.name}] [${entry.level}] ${entry.message}${dataStr}`;
|
|
418
|
-
switch (entry.level) {
|
|
419
|
-
case "DEBUG" /* DEBUG */:
|
|
420
|
-
console.debug(msg);
|
|
421
|
-
break;
|
|
422
|
-
case "INFO" /* INFO */:
|
|
423
|
-
console.info(msg);
|
|
424
|
-
break;
|
|
425
|
-
case "WARN" /* WARN */:
|
|
426
|
-
console.warn(msg);
|
|
427
|
-
break;
|
|
428
|
-
case "ERROR" /* ERROR */:
|
|
429
|
-
console.error(msg);
|
|
430
|
-
break;
|
|
431
|
-
}
|
|
432
|
-
}
|
|
433
|
-
async writeToFile(entry) {
|
|
434
|
-
await this.ensureLogDirectory();
|
|
435
|
-
try {
|
|
436
|
-
await fs3.appendFile(this.currentLogFile, JSON.stringify(entry) + "\n");
|
|
437
|
-
} catch (error) {
|
|
438
|
-
console.error(
|
|
439
|
-
`\u30ED\u30B0\u306E\u66F8\u304D\u8FBC\u307F\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${error instanceof Error ? error.message : String(error)}`
|
|
440
|
-
);
|
|
441
|
-
}
|
|
442
|
-
}
|
|
443
|
-
debug(message, data) {
|
|
444
|
-
return this.writeLog(this.formatLogEntry("DEBUG" /* DEBUG */, message, data));
|
|
445
|
-
}
|
|
446
|
-
info(message, data) {
|
|
447
|
-
return this.writeLog(this.formatLogEntry("INFO" /* INFO */, message, data));
|
|
448
|
-
}
|
|
449
|
-
warn(message, data) {
|
|
450
|
-
return this.writeLog(this.formatLogEntry("WARN" /* WARN */, message, data));
|
|
451
|
-
}
|
|
452
|
-
error(message, data) {
|
|
453
|
-
return this.writeLog(this.formatLogEntry("ERROR" /* ERROR */, message, data));
|
|
454
|
-
}
|
|
455
|
-
static debug(message, data) {
|
|
456
|
-
return _Logger.getInstance({ name: _Logger.currentContext }).debug(message, data);
|
|
457
|
-
}
|
|
458
|
-
static info(message, data) {
|
|
459
|
-
return _Logger.getInstance({ name: _Logger.currentContext }).info(message, data);
|
|
460
|
-
}
|
|
461
|
-
static warn(message, data) {
|
|
462
|
-
return _Logger.getInstance({ name: _Logger.currentContext }).warn(message, data);
|
|
463
|
-
}
|
|
464
|
-
static error(message, data) {
|
|
465
|
-
return _Logger.getInstance({ name: _Logger.currentContext }).error(message, data);
|
|
466
|
-
}
|
|
467
|
-
getLatestLogFilePath() {
|
|
468
|
-
return this.currentLogFile;
|
|
469
|
-
}
|
|
470
|
-
async getLogEntries(minLevel = "INFO" /* INFO */, maxEntries = 100) {
|
|
471
|
-
try {
|
|
472
|
-
await this.ensureLogDirectory();
|
|
473
|
-
const content = await fs3.readFile(this.currentLogFile, "utf-8");
|
|
474
|
-
const lines = content.trim().split("\n");
|
|
475
|
-
const priority = {
|
|
476
|
-
["DEBUG" /* DEBUG */]: 0,
|
|
477
|
-
["INFO" /* INFO */]: 1,
|
|
478
|
-
["WARN" /* WARN */]: 2,
|
|
479
|
-
["ERROR" /* ERROR */]: 3
|
|
480
|
-
};
|
|
481
|
-
const entries = [];
|
|
482
|
-
for (let i = lines.length - 1; i >= 0 && entries.length < maxEntries; i--) {
|
|
483
|
-
try {
|
|
484
|
-
const entry = JSON.parse(lines[i]);
|
|
485
|
-
if (priority[entry.level] >= priority[minLevel]) entries.unshift(entry);
|
|
486
|
-
} catch {
|
|
487
|
-
continue;
|
|
488
|
-
}
|
|
489
|
-
}
|
|
490
|
-
return entries;
|
|
491
|
-
} catch (error) {
|
|
492
|
-
if (error.code === "ENOENT") return [];
|
|
493
|
-
throw error;
|
|
494
|
-
}
|
|
495
|
-
}
|
|
496
|
-
};
|
|
497
|
-
|
|
498
|
-
// src/utils/azure-chat.ts
|
|
499
|
-
var AzureChatClient = class {
|
|
500
|
-
client;
|
|
501
|
-
deploymentName;
|
|
502
|
-
logger;
|
|
503
|
-
constructor(config) {
|
|
504
|
-
this.client = new AzureOpenAI({
|
|
505
|
-
endpoint: config.endpoint,
|
|
506
|
-
apiKey: config.apiKey,
|
|
507
|
-
apiVersion: config.apiVersion,
|
|
508
|
-
deployment: config.deploymentName
|
|
509
|
-
});
|
|
510
|
-
this.deploymentName = config.deploymentName;
|
|
511
|
-
this.logger = Logger.getInstance({ name: "azure-chat" });
|
|
512
|
-
}
|
|
513
|
-
/**
|
|
514
|
-
* Generates a detailed image-generation prompt from a short theme description.
|
|
515
|
-
* Optionally accepts additional context to guide prompt generation.
|
|
516
|
-
*/
|
|
517
|
-
async generatePrompt(theme, context = "") {
|
|
518
|
-
if (!theme) throw new Error("\u30C6\u30FC\u30DE\u304C\u7A7A\u3067\u3059");
|
|
519
|
-
const prompt = `
|
|
520
|
-
Generate a detailed image generation prompt based on the following information.
|
|
521
|
-
|
|
522
|
-
Theme: ${theme}
|
|
523
|
-
${context ? `Context:
|
|
524
|
-
${context}
|
|
525
|
-
` : ""}
|
|
526
|
-
Please generate a prompt that meets the following criteria:
|
|
527
|
-
1. Include specific and detailed descriptions
|
|
528
|
-
2. Clearly specify the image style and atmosphere
|
|
529
|
-
3. Include all necessary elements
|
|
530
|
-
4. Output in English
|
|
531
|
-
5. Focus on visual elements and composition
|
|
532
|
-
6. Include lighting and color descriptions
|
|
533
|
-
7. Specify the mood and emotional tone
|
|
534
|
-
8. Limit the output to approximately 1500 characters
|
|
535
|
-
|
|
536
|
-
Prompt:
|
|
537
|
-
`;
|
|
538
|
-
try {
|
|
539
|
-
const response = await this.client.chat.completions.create({
|
|
540
|
-
model: this.deploymentName,
|
|
541
|
-
messages: [{ role: "user", content: prompt }]
|
|
542
|
-
});
|
|
543
|
-
return response.choices[0]?.message?.content ?? "";
|
|
544
|
-
} catch (error) {
|
|
545
|
-
this.logger.error("\u30D7\u30ED\u30F3\u30D7\u30C8\u751F\u6210\u306B\u5931\u6557\u3057\u307E\u3057\u305F", { error });
|
|
546
|
-
throw new Error("\u30D7\u30ED\u30F3\u30D7\u30C8\u306E\u751F\u6210\u306B\u5931\u6557\u3057\u307E\u3057\u305F");
|
|
547
|
-
}
|
|
548
|
-
}
|
|
549
|
-
/**
|
|
550
|
-
* Generates a sanitized file name (lowercase alphanumeric + hyphens only) from a theme.
|
|
551
|
-
*/
|
|
552
|
-
async generateFileName(theme, maxLength = 40) {
|
|
553
|
-
if (!theme) throw new Error("\u30C6\u30FC\u30DE\u304C\u7A7A\u3067\u3059");
|
|
554
|
-
try {
|
|
555
|
-
const response = await this.client.chat.completions.create({
|
|
556
|
-
model: this.deploymentName,
|
|
557
|
-
messages: [
|
|
558
|
-
{
|
|
559
|
-
role: "user",
|
|
560
|
-
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
|
|
561
|
-
|
|
562
|
-
\u30C6\u30FC\u30DE: ${theme}
|
|
563
|
-
|
|
564
|
-
\u30D5\u30A1\u30A4\u30EB\u540D:`
|
|
565
|
-
}
|
|
566
|
-
]
|
|
567
|
-
});
|
|
568
|
-
let fileName = (response.choices[0]?.message?.content ?? "").trim();
|
|
569
|
-
fileName = fileName.toLowerCase().replace(/[^a-z0-9-]/g, "").replace(/-+/g, "-").replace(/^-|-$/g, "");
|
|
570
|
-
if (fileName.length > maxLength) fileName = fileName.substring(0, maxLength);
|
|
571
|
-
return fileName || "image";
|
|
572
|
-
} catch (error) {
|
|
573
|
-
this.logger.error("\u30D5\u30A1\u30A4\u30EB\u540D\u751F\u6210\u306B\u5931\u6557\u3057\u307E\u3057\u305F", { error });
|
|
574
|
-
throw new Error("\u30D5\u30A1\u30A4\u30EB\u540D\u306E\u751F\u6210\u306B\u5931\u6557\u3057\u307E\u3057\u305F");
|
|
575
|
-
}
|
|
576
|
-
}
|
|
577
|
-
/**
|
|
578
|
-
* Generates a detailed explanation of an image using multimodal (vision) input.
|
|
579
|
-
* The explanation language is controlled by the `lang` parameter.
|
|
580
|
-
*/
|
|
581
|
-
async generateExplanation(imageData, lang = "ja", context) {
|
|
582
|
-
try {
|
|
583
|
-
const response = await this.client.chat.completions.create({
|
|
584
|
-
model: this.deploymentName,
|
|
585
|
-
messages: [
|
|
586
|
-
{
|
|
587
|
-
role: "user",
|
|
588
|
-
content: [
|
|
589
|
-
{
|
|
590
|
-
type: "image_url",
|
|
591
|
-
image_url: { url: `data:${imageData.mimeType};base64,${imageData.data}` }
|
|
592
|
-
},
|
|
593
|
-
{
|
|
594
|
-
type: "text",
|
|
595
|
-
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 ? `
|
|
596
|
-
|
|
597
|
-
\u30B3\u30F3\u30C6\u30AD\u30B9\u30C8\u60C5\u5831:
|
|
598
|
-
${context}` : ""}`
|
|
599
|
-
}
|
|
600
|
-
]
|
|
601
|
-
}
|
|
602
|
-
]
|
|
603
|
-
});
|
|
604
|
-
return response.choices[0]?.message?.content ?? "";
|
|
605
|
-
} catch (error) {
|
|
606
|
-
this.logger.error("\u753B\u50CF\u8AAC\u660E\u306E\u751F\u6210\u306B\u5931\u6557\u3057\u307E\u3057\u305F", { error });
|
|
607
|
-
throw new Error("\u753B\u50CF\u306E\u8AAC\u660E\u751F\u6210\u306B\u5931\u6557\u3057\u307E\u3057\u305F");
|
|
608
|
-
}
|
|
609
|
-
}
|
|
610
|
-
};
|
|
611
|
-
|
|
612
|
-
// src/utils/azure-image.ts
|
|
613
|
-
import { AzureOpenAI as AzureOpenAI2 } from "openai";
|
|
614
|
-
var AzureImageClient = class {
|
|
615
|
-
client;
|
|
616
|
-
config;
|
|
617
|
-
logger;
|
|
618
|
-
constructor(config) {
|
|
619
|
-
this.config = config;
|
|
620
|
-
this.client = new AzureOpenAI2({
|
|
621
|
-
endpoint: config.endpoint,
|
|
622
|
-
apiKey: config.apiKey,
|
|
623
|
-
apiVersion: config.imageApiVersion,
|
|
624
|
-
deployment: config.imageDeploymentName
|
|
625
|
-
});
|
|
626
|
-
this.logger = Logger.getInstance({ name: "azure-image" });
|
|
627
|
-
}
|
|
628
|
-
/**
|
|
629
|
-
* Generates an image from a text prompt using the Azure OpenAI SDK.
|
|
630
|
-
* Returns raw image bytes as a Uint8Array.
|
|
631
|
-
*/
|
|
632
|
-
async generateImage(prompt, options) {
|
|
633
|
-
const { size = "1024x1024", quality = "high" } = options;
|
|
634
|
-
this.logger.debug("\u753B\u50CF\u751F\u6210\u30EA\u30AF\u30A8\u30B9\u30C8", { prompt: prompt.substring(0, 100), size, quality });
|
|
635
|
-
try {
|
|
636
|
-
const response = await this.client.images.generate({
|
|
637
|
-
model: this.config.imageDeploymentName,
|
|
638
|
-
prompt,
|
|
639
|
-
n: 1,
|
|
640
|
-
size,
|
|
641
|
-
quality,
|
|
642
|
-
output_format: "png"
|
|
643
|
-
});
|
|
644
|
-
if (!response.data || response.data.length === 0 || !response.data[0].b64_json) {
|
|
645
|
-
throw new Error("\u753B\u50CF\u30C7\u30FC\u30BF\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093");
|
|
646
|
-
}
|
|
647
|
-
const b64 = response.data[0].b64_json;
|
|
648
|
-
const binary = atob(b64);
|
|
649
|
-
const bytes = new Uint8Array(binary.length);
|
|
650
|
-
for (let i = 0; i < binary.length; i++) {
|
|
651
|
-
bytes[i] = binary.charCodeAt(i);
|
|
652
|
-
}
|
|
653
|
-
return bytes;
|
|
654
|
-
} catch (error) {
|
|
655
|
-
if (error instanceof Error && error.message === "\u753B\u50CF\u30C7\u30FC\u30BF\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093") throw error;
|
|
656
|
-
this.logger.error("\u753B\u50CF\u751F\u6210\u306B\u5931\u6557\u3057\u307E\u3057\u305F", { error });
|
|
657
|
-
throw new Error(
|
|
658
|
-
`\u753B\u50CF\u751F\u6210\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${error instanceof Error ? error.message : String(error)}`
|
|
659
|
-
);
|
|
660
|
-
}
|
|
661
|
-
}
|
|
662
|
-
/**
|
|
663
|
-
* Edits an existing image using the Azure OpenAI REST API.
|
|
664
|
-
* Uses fetch + FormData because the SDK's image editing support is unreliable.
|
|
665
|
-
* Returns raw image bytes as a Uint8Array.
|
|
666
|
-
*/
|
|
667
|
-
async editImage(imageBuffer, prompt, options = {}) {
|
|
668
|
-
const { size = "1024x1024" } = options;
|
|
669
|
-
this.logger.debug("\u753B\u50CF\u7DE8\u96C6\u30EA\u30AF\u30A8\u30B9\u30C8 (REST API)", { prompt: prompt.substring(0, 100), size });
|
|
670
|
-
const url = `${this.config.endpoint}/openai/deployments/${this.config.imageDeploymentName}/images/edits?api-version=${this.config.imageApiVersion}`;
|
|
671
|
-
const blob = new Blob([imageBuffer], { type: "image/png" });
|
|
672
|
-
const formData = new FormData();
|
|
673
|
-
formData.append("image", blob, "image.png");
|
|
674
|
-
formData.append("prompt", prompt);
|
|
675
|
-
formData.append("size", size);
|
|
676
|
-
try {
|
|
677
|
-
const response = await fetch(url, {
|
|
678
|
-
method: "POST",
|
|
679
|
-
headers: { "api-key": this.config.apiKey },
|
|
680
|
-
body: formData
|
|
681
|
-
});
|
|
682
|
-
if (!response.ok) {
|
|
683
|
-
const errorText = await response.text();
|
|
684
|
-
throw new Error(`Azure API error (${response.status}): ${errorText}`);
|
|
685
|
-
}
|
|
686
|
-
const json = await response.json();
|
|
687
|
-
if (!json.data || json.data.length === 0 || !json.data[0].b64_json) {
|
|
688
|
-
throw new Error("\u753B\u50CF\u30C7\u30FC\u30BF\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093");
|
|
689
|
-
}
|
|
690
|
-
const b64 = json.data[0].b64_json;
|
|
691
|
-
const binary = atob(b64);
|
|
692
|
-
const bytes = new Uint8Array(binary.length);
|
|
693
|
-
for (let i = 0; i < binary.length; i++) {
|
|
694
|
-
bytes[i] = binary.charCodeAt(i);
|
|
695
|
-
}
|
|
696
|
-
return bytes;
|
|
697
|
-
} catch (error) {
|
|
698
|
-
if (error instanceof Error && error.message === "\u753B\u50CF\u30C7\u30FC\u30BF\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093") throw error;
|
|
699
|
-
this.logger.error("\u753B\u50CF\u7DE8\u96C6\u306B\u5931\u6557\u3057\u307E\u3057\u305F", { error });
|
|
700
|
-
throw new Error(
|
|
701
|
-
`\u753B\u50CF\u7DE8\u96C6\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${error instanceof Error ? error.message : String(error)}`
|
|
702
|
-
);
|
|
703
|
-
}
|
|
704
|
-
}
|
|
705
|
-
};
|
|
706
|
-
|
|
707
256
|
// src/utils/output.ts
|
|
708
257
|
function createSuccessOutput(command, result) {
|
|
709
258
|
return { success: true, command, result };
|
|
@@ -727,7 +276,7 @@ function imageEditCommand() {
|
|
|
727
276
|
).addOption(
|
|
728
277
|
new Option("-s, --size <size>", "\u51FA\u529B\u30B5\u30A4\u30BA").choices(["1024x1024", "1536x1024", "1024x1536"]).default("1024x1024")
|
|
729
278
|
).option("--json", "JSON\u5F62\u5F0F\u3067\u51FA\u529B", false).option("--dry-run", "\u5B9F\u884C\u305B\u305A\u306B\u8A2D\u5B9A\u3092\u78BA\u8A8D", false).action(async (filePath, prompt, options) => {
|
|
730
|
-
const ext =
|
|
279
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
731
280
|
if (!SUPPORTED_EXTENSIONS.includes(ext)) {
|
|
732
281
|
const msg = `\u30B5\u30DD\u30FC\u30C8\u3055\u308C\u3066\u3044\u306A\u3044\u30D5\u30A1\u30A4\u30EB\u5F62\u5F0F\u3067\u3059: ${ext}`;
|
|
733
282
|
if (options.json) {
|
|
@@ -776,18 +325,18 @@ function imageEditCommand() {
|
|
|
776
325
|
const azureConfig = await getAzureConfig();
|
|
777
326
|
const chatClient = new AzureChatClient(azureConfig);
|
|
778
327
|
const imageClient = new AzureImageClient(azureConfig);
|
|
779
|
-
const imageBuffer = Buffer.from(await
|
|
328
|
+
const imageBuffer = Buffer.from(await fs.readFile(filePath));
|
|
780
329
|
const editedData = await imageClient.editImage(imageBuffer, prompt, {
|
|
781
330
|
size: effectiveSize
|
|
782
331
|
});
|
|
783
|
-
const baseName =
|
|
332
|
+
const baseName = path.basename(filePath, ext);
|
|
784
333
|
const fileName = await chatClient.generateFileName(`${baseName} edited`);
|
|
785
334
|
let outputPath;
|
|
786
335
|
if (options.output) {
|
|
787
336
|
try {
|
|
788
|
-
const
|
|
789
|
-
if (
|
|
790
|
-
outputPath =
|
|
337
|
+
const stat3 = await fs.stat(options.output);
|
|
338
|
+
if (stat3.isDirectory()) {
|
|
339
|
+
outputPath = path.join(options.output, `${fileName}.${effectiveFormat}`);
|
|
791
340
|
} else {
|
|
792
341
|
outputPath = options.output;
|
|
793
342
|
}
|
|
@@ -837,41 +386,8 @@ function imageEditCommand() {
|
|
|
837
386
|
}
|
|
838
387
|
|
|
839
388
|
// src/commands/image/explain.ts
|
|
840
|
-
import * as
|
|
389
|
+
import * as fs2 from "fs/promises";
|
|
841
390
|
import { Command as Command3 } from "commander";
|
|
842
|
-
|
|
843
|
-
// src/utils/image.ts
|
|
844
|
-
import * as fs5 from "fs/promises";
|
|
845
|
-
async function readImageFile(filePath) {
|
|
846
|
-
try {
|
|
847
|
-
const buffer = await fs5.readFile(filePath);
|
|
848
|
-
return {
|
|
849
|
-
data: buffer.toString("base64"),
|
|
850
|
-
mimeType: getMimeType(filePath)
|
|
851
|
-
};
|
|
852
|
-
} catch (error) {
|
|
853
|
-
if (error instanceof Error && error.message.startsWith("\u30B5\u30DD\u30FC\u30C8\u3055\u308C\u3066\u3044\u306A\u3044")) {
|
|
854
|
-
throw error;
|
|
855
|
-
}
|
|
856
|
-
throw new Error(
|
|
857
|
-
`\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)}`
|
|
858
|
-
);
|
|
859
|
-
}
|
|
860
|
-
}
|
|
861
|
-
function getMimeType(filePath) {
|
|
862
|
-
const ext = filePath.toLowerCase().split(".").pop();
|
|
863
|
-
const map = {
|
|
864
|
-
jpg: "image/jpeg",
|
|
865
|
-
jpeg: "image/jpeg",
|
|
866
|
-
png: "image/png",
|
|
867
|
-
gif: "image/gif",
|
|
868
|
-
webp: "image/webp"
|
|
869
|
-
};
|
|
870
|
-
if (ext && map[ext]) return map[ext];
|
|
871
|
-
throw new Error(`\u30B5\u30DD\u30FC\u30C8\u3055\u308C\u3066\u3044\u306A\u3044\u30D5\u30A1\u30A4\u30EB\u5F62\u5F0F\u3067\u3059: .${ext}`);
|
|
872
|
-
}
|
|
873
|
-
|
|
874
|
-
// src/commands/image/explain.ts
|
|
875
391
|
function imageExplainCommand() {
|
|
876
392
|
return new Command3("explain").description("\u753B\u50CF\u306E\u5185\u5BB9\u3092\u8AAC\u660E").argument("<file>", "\u753B\u50CF\u30D5\u30A1\u30A4\u30EB\u306E\u30D1\u30B9").option(
|
|
877
393
|
"-l, --lang <lang>",
|
|
@@ -889,7 +405,7 @@ function imageExplainCommand() {
|
|
|
889
405
|
let context;
|
|
890
406
|
if (options.context) {
|
|
891
407
|
if (options.context.endsWith(".md") || options.context.endsWith(".txt")) {
|
|
892
|
-
context = await
|
|
408
|
+
context = await fs2.readFile(options.context, "utf-8");
|
|
893
409
|
} else {
|
|
894
410
|
context = options.context;
|
|
895
411
|
}
|
|
@@ -906,7 +422,7 @@ function imageExplainCommand() {
|
|
|
906
422
|
${explanation}`;
|
|
907
423
|
}
|
|
908
424
|
if (options.output) {
|
|
909
|
-
await
|
|
425
|
+
await fs2.writeFile(options.output, output, "utf-8");
|
|
910
426
|
console.log(`\u8AAC\u660E\u3092\u4FDD\u5B58\u3057\u307E\u3057\u305F: ${options.output}`);
|
|
911
427
|
} else {
|
|
912
428
|
console.log(output);
|
|
@@ -919,74 +435,9 @@ ${explanation}`;
|
|
|
919
435
|
}
|
|
920
436
|
|
|
921
437
|
// src/commands/image/gen.ts
|
|
922
|
-
import * as
|
|
923
|
-
import * as
|
|
438
|
+
import * as fs3 from "fs/promises";
|
|
439
|
+
import * as path2 from "path";
|
|
924
440
|
import { Command as Command4, Option as Option2 } from "commander";
|
|
925
|
-
|
|
926
|
-
// src/utils/preset.ts
|
|
927
|
-
import * as fs7 from "fs/promises";
|
|
928
|
-
import * as path4 from "path";
|
|
929
|
-
var BUILTIN_PRESETS = {
|
|
930
|
-
"builtin:square": { size: "1024x1024", quality: "high" },
|
|
931
|
-
"builtin:landscape": { size: "1536x1024", quality: "high" },
|
|
932
|
-
"builtin:portrait": { size: "1024x1536", quality: "high" },
|
|
933
|
-
"builtin:draft": { size: "1024x1024", quality: "low" },
|
|
934
|
-
"builtin:photo": { size: "1536x1024", quality: "high" }
|
|
935
|
-
};
|
|
936
|
-
function getPresetsPath() {
|
|
937
|
-
return path4.join(getConfigDir(), "presets.json");
|
|
938
|
-
}
|
|
939
|
-
async function loadPresets() {
|
|
940
|
-
try {
|
|
941
|
-
const text = await fs7.readFile(getPresetsPath(), "utf-8");
|
|
942
|
-
return JSON.parse(text);
|
|
943
|
-
} catch {
|
|
944
|
-
return {};
|
|
945
|
-
}
|
|
946
|
-
}
|
|
947
|
-
async function savePresets(presets) {
|
|
948
|
-
const p = getPresetsPath();
|
|
949
|
-
await fs7.mkdir(path4.dirname(p), { recursive: true });
|
|
950
|
-
await fs7.writeFile(p, JSON.stringify(presets, null, 2));
|
|
951
|
-
}
|
|
952
|
-
async function getPreset(name) {
|
|
953
|
-
if (name.startsWith("builtin:") && BUILTIN_PRESETS[name]) {
|
|
954
|
-
return BUILTIN_PRESETS[name];
|
|
955
|
-
}
|
|
956
|
-
const presets = await loadPresets();
|
|
957
|
-
return presets[name] || null;
|
|
958
|
-
}
|
|
959
|
-
async function savePreset(name, preset) {
|
|
960
|
-
if (name.startsWith("builtin:")) {
|
|
961
|
-
throw new Error("\u30D3\u30EB\u30C8\u30A4\u30F3\u30D7\u30EA\u30BB\u30C3\u30C8\u306F\u4E0A\u66F8\u304D\u3067\u304D\u307E\u305B\u3093");
|
|
962
|
-
}
|
|
963
|
-
const presets = await loadPresets();
|
|
964
|
-
presets[name] = preset;
|
|
965
|
-
await savePresets(presets);
|
|
966
|
-
}
|
|
967
|
-
async function deletePreset(name) {
|
|
968
|
-
if (name.startsWith("builtin:")) {
|
|
969
|
-
throw new Error("\u30D3\u30EB\u30C8\u30A4\u30F3\u30D7\u30EA\u30BB\u30C3\u30C8\u306F\u524A\u9664\u3067\u304D\u307E\u305B\u3093");
|
|
970
|
-
}
|
|
971
|
-
const presets = await loadPresets();
|
|
972
|
-
if (!presets[name]) return false;
|
|
973
|
-
delete presets[name];
|
|
974
|
-
await savePresets(presets);
|
|
975
|
-
return true;
|
|
976
|
-
}
|
|
977
|
-
async function listAllPresets() {
|
|
978
|
-
const result = [];
|
|
979
|
-
for (const [name, preset] of Object.entries(BUILTIN_PRESETS)) {
|
|
980
|
-
result.push({ name, preset, builtin: true });
|
|
981
|
-
}
|
|
982
|
-
const presets = await loadPresets();
|
|
983
|
-
for (const [name, preset] of Object.entries(presets)) {
|
|
984
|
-
result.push({ name, preset, builtin: false });
|
|
985
|
-
}
|
|
986
|
-
return result;
|
|
987
|
-
}
|
|
988
|
-
|
|
989
|
-
// src/commands/image/gen.ts
|
|
990
441
|
var VALID_FORMATS = ["png", "jpg", "webp"];
|
|
991
442
|
function validateImageFormat(ext, specifiedFormat) {
|
|
992
443
|
if (!VALID_FORMATS.includes(ext)) {
|
|
@@ -999,28 +450,28 @@ function validateImageFormat(ext, specifiedFormat) {
|
|
|
999
450
|
}
|
|
1000
451
|
}
|
|
1001
452
|
async function resolveOutputPath(outputPath, defaultFormat, fileName) {
|
|
1002
|
-
let
|
|
453
|
+
let format = defaultFormat;
|
|
1003
454
|
if (!outputPath) {
|
|
1004
|
-
return { path: `${fileName}.${
|
|
455
|
+
return { path: `${fileName}.${format}`, format };
|
|
1005
456
|
}
|
|
1006
457
|
try {
|
|
1007
|
-
const
|
|
1008
|
-
if (
|
|
1009
|
-
return { path:
|
|
458
|
+
const stat3 = await fs3.stat(outputPath);
|
|
459
|
+
if (stat3.isDirectory()) {
|
|
460
|
+
return { path: path2.join(outputPath, `${fileName}.${format}`), format };
|
|
1010
461
|
}
|
|
1011
462
|
} catch (error) {
|
|
1012
463
|
if (error.code !== "ENOENT") {
|
|
1013
464
|
throw error;
|
|
1014
465
|
}
|
|
1015
466
|
}
|
|
1016
|
-
const ext =
|
|
467
|
+
const ext = path2.extname(outputPath).toLowerCase().slice(1);
|
|
1017
468
|
if (ext) {
|
|
1018
469
|
validateImageFormat(ext, defaultFormat);
|
|
1019
|
-
|
|
470
|
+
format = ext;
|
|
1020
471
|
} else {
|
|
1021
|
-
outputPath = `${outputPath}.${
|
|
472
|
+
outputPath = `${outputPath}.${format}`;
|
|
1022
473
|
}
|
|
1023
|
-
return { path: outputPath, format
|
|
474
|
+
return { path: outputPath, format };
|
|
1024
475
|
}
|
|
1025
476
|
function imageGenCommand() {
|
|
1026
477
|
return new Command4("gen").description("\u30C6\u30AD\u30B9\u30C8\u304B\u3089\u753B\u50CF\u3092\u751F\u6210").argument("<theme>", "\u753B\u50CF\u751F\u6210\u306E\u30C6\u30FC\u30DE").option("-c, --context <file>", "\u30B3\u30F3\u30C6\u30AD\u30B9\u30C8\u30D5\u30A1\u30A4\u30EB\u306E\u30D1\u30B9").option("-o, --output <path>", "\u51FA\u529B\u30D1\u30B9\uFF08\u30D5\u30A1\u30A4\u30EB\u307E\u305F\u306F\u30C7\u30A3\u30EC\u30AF\u30C8\u30EA\uFF09").addOption(
|
|
@@ -1101,7 +552,7 @@ imgen preset list \u3067\u4E00\u89A7\u3092\u78BA\u8A8D\u3057\u3066\u304F\u3060\u
|
|
|
1101
552
|
quality: effectiveQuality
|
|
1102
553
|
});
|
|
1103
554
|
const fileName = await chatClient.generateFileName(theme);
|
|
1104
|
-
const { path: outputPath, format
|
|
555
|
+
const { path: outputPath, format } = await resolveOutputPath(
|
|
1105
556
|
options.output,
|
|
1106
557
|
effectiveFormat,
|
|
1107
558
|
fileName
|
|
@@ -1109,7 +560,7 @@ imgen preset list \u3067\u4E00\u89A7\u3092\u78BA\u8A8D\u3057\u3066\u304F\u3060\u
|
|
|
1109
560
|
const finalOutputPath = await saveFileWithUniqueNameIfExists(outputPath, imageData);
|
|
1110
561
|
await logger.info("\u753B\u50CF\u3092\u751F\u6210\u3057\u307E\u3057\u305F", {
|
|
1111
562
|
path: finalOutputPath,
|
|
1112
|
-
format
|
|
563
|
+
format,
|
|
1113
564
|
size: effectiveSize,
|
|
1114
565
|
quality: effectiveQuality,
|
|
1115
566
|
...options.preset ? { preset: options.preset } : {}
|
|
@@ -1118,7 +569,7 @@ imgen preset list \u3067\u4E00\u89A7\u3092\u78BA\u8A8D\u3057\u3066\u304F\u3060\u
|
|
|
1118
569
|
printJson(
|
|
1119
570
|
createSuccessOutput("image gen", {
|
|
1120
571
|
path: finalOutputPath,
|
|
1121
|
-
format
|
|
572
|
+
format,
|
|
1122
573
|
size: effectiveSize,
|
|
1123
574
|
quality: effectiveQuality,
|
|
1124
575
|
...options.preset ? { preset: options.preset } : {}
|