sns-auto-builder 1.0.9 → 1.1.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/package.json +1 -1
- package/scripts/chatgpt-image-gen.js +81 -6
package/package.json
CHANGED
|
@@ -234,11 +234,48 @@ async function attachRef(page, refPaths) {
|
|
|
234
234
|
+ ` 그때 화면에 있던 file input 목록이 남습니다 — 빌더 첫 화면의 로그 버튼으로 받으세요.`);
|
|
235
235
|
}
|
|
236
236
|
|
|
237
|
+
// 보내기 전에 앞 턴이 끝났는지 보고, 보낸 뒤에 정말 나갔는지 확인한다.
|
|
238
|
+
// 예전에는 클릭·타이핑·Enter 만 하고 끝이라, Enter 가 씹혀도 그대로 다음 단계로 갔다.
|
|
239
|
+
// 앞 턴이 "응답 중" 으로 굳어 있으면 Enter 가 아예 안 먹는다(실측). "답변 중지" 를
|
|
240
|
+
// 누르면 풀리고, 그때 입력칸이 다시 그려지므로 셀렉터를 다시 기다려야 한다.
|
|
241
|
+
async function clearBusy(page) {
|
|
242
|
+
console.warn(' 앞 턴이 응답 중 상태로 굳어 있습니다 — "답변 중지" 로 정리합니다.');
|
|
243
|
+
await page.click(STOP_BTN).catch(() => { /* 그새 사라졌으면 그만 */ });
|
|
244
|
+
await waitForIdle(page, 15_000);
|
|
245
|
+
await page.waitForSelector('#prompt-textarea', { timeout: 30_000 }).catch(() => {});
|
|
246
|
+
}
|
|
247
|
+
|
|
237
248
|
async function sendPrompt(page, text) {
|
|
238
|
-
await page.click('#prompt-textarea');
|
|
239
249
|
// 프롬프트는 항상 한 줄로 만든다 — 개행이 들어가면 Enter 가 도중에 전송해버린다.
|
|
240
|
-
|
|
241
|
-
await page
|
|
250
|
+
const one = text.replace(/\s*\n\s*/g, ' ');
|
|
251
|
+
if (!(await waitForIdle(page, IDLE_WAIT_MS))) await clearBusy(page);
|
|
252
|
+
|
|
253
|
+
for (let attempt = 1; attempt <= 2; attempt++) {
|
|
254
|
+
await page.waitForSelector('#prompt-textarea', { timeout: 30_000 });
|
|
255
|
+
// 중지 뒤에는 앞서 친 글자가 남아 있을 수도, 비어 있을 수도 있다. 다르면 지우고 다시 친다.
|
|
256
|
+
const cur = await page.evaluate(() => document.getElementById('prompt-textarea').innerText.trim());
|
|
257
|
+
if (cur !== one) {
|
|
258
|
+
await page.click('#prompt-textarea');
|
|
259
|
+
if (cur) {
|
|
260
|
+
await page.keyboard.down('Control'); await page.keyboard.press('KeyA'); await page.keyboard.up('Control');
|
|
261
|
+
await page.keyboard.press('Backspace');
|
|
262
|
+
}
|
|
263
|
+
await page.type('#prompt-textarea', one);
|
|
264
|
+
}
|
|
265
|
+
await page.keyboard.press('Enter');
|
|
266
|
+
|
|
267
|
+
// **입력칸이 비는 것만 본다.** 우리가 넣은 글자가 실제로 나갔다는 뜻이라 확실하다.
|
|
268
|
+
// stop 버튼은 앞 턴 것이 남아 있을 수 있어 전송 확인에 쓰면 안 된다.
|
|
269
|
+
const sent = await page.waitForFunction(() => {
|
|
270
|
+
const c = document.getElementById('prompt-textarea');
|
|
271
|
+
return !!c && !c.innerText.trim();
|
|
272
|
+
}, { timeout: 15_000, polling: 500 }).then(() => true).catch(() => false);
|
|
273
|
+
if (sent) return;
|
|
274
|
+
|
|
275
|
+
console.warn(` (재시도 ${attempt}/2) Enter 가 먹지 않았습니다 — 입력칸에 글자가 그대로입니다`);
|
|
276
|
+
await clearBusy(page);
|
|
277
|
+
}
|
|
278
|
+
throw new Error('프롬프트를 전송하지 못했습니다 — ChatGPT 가 계속 응답 중 상태입니다');
|
|
242
279
|
}
|
|
243
280
|
|
|
244
281
|
// 새 이미지를 기다렸다가 **이 프롬프트에 해당하는 1장**을 골라 돌려준다.
|
|
@@ -249,6 +286,25 @@ async function sendPrompt(page, text) {
|
|
|
249
286
|
// 여러 장이 나왔으면 경고를 남긴다 — 프롬프트가 다장 생성을 유도했다는 신호다.
|
|
250
287
|
const SETTLE_MS = 12_000; // 개수가 이만큼 안 늘면 그 턴은 끝난 것으로 본다
|
|
251
288
|
|
|
289
|
+
// 응답하는 동안 뜨는 "답변 중지" 버튼.
|
|
290
|
+
//
|
|
291
|
+
// 보통은 응답이 끝나면 사라지지만, **안 사라진 채 굳는 경우가 있다** (생성 중에 페이지를
|
|
292
|
+
// 새로고침하면 그렇게 된다 — 실측). 그 상태에서는 Enter 가 아예 안 먹고 입력칸에 글자만
|
|
293
|
+
// 남는다. 그래서 이 신호로 실행을 막지는 않고(막으면 영원히 안 끝난다), 굳었으면
|
|
294
|
+
// clearBusy 로 풀고 간다.
|
|
295
|
+
const STOP_BTN = '[data-testid="stop-button"]';
|
|
296
|
+
const IDLE_GRACE_MS = 20_000; // 버튼이 실제로 사라진 뒤 이만큼 더 봤는데도 이미지가 없으면 실패
|
|
297
|
+
const IDLE_WAIT_MS = 90_000; // 앞 턴을 기다려보는 상한. 넘으면 경고만 하고 그냥 보낸다
|
|
298
|
+
|
|
299
|
+
const isStreaming = (page) => page.$(STOP_BTN).then((h) => !!h).catch(() => false);
|
|
300
|
+
|
|
301
|
+
// 앞 턴이 끝나길 기다린다. **던지지 않는다** — 위 이유로 이 신호는 조언일 뿐이다.
|
|
302
|
+
async function waitForIdle(page, timeoutMs) {
|
|
303
|
+
return page
|
|
304
|
+
.waitForFunction((sel) => !document.querySelector(sel), { timeout: timeoutMs, polling: 1000 }, STOP_BTN)
|
|
305
|
+
.then(() => true).catch(() => false);
|
|
306
|
+
}
|
|
307
|
+
|
|
252
308
|
// ChatGPT 가 생성에 실패하면 본문에 영어 안내가 뜨고 이미지는 안 나온다.
|
|
253
309
|
// 그걸 못 읽으면 우리는 타임아웃까지 5분을 그냥 기다린 뒤 "안 나옴" 이라고만 말하게 된다.
|
|
254
310
|
// 실제 문구를 그대로 사용자에게 전달해야 원인 파악이 된다.
|
|
@@ -276,7 +332,7 @@ const readPageError = (page) => page.evaluate((needles) => {
|
|
|
276
332
|
// 저장돼, 3장만 만들어졌는데 4장이 나온 것처럼 끝난 적이 있다.
|
|
277
333
|
async function waitForNewImage(page, before, timeoutMs, used = new Set()) {
|
|
278
334
|
const deadline = Date.now() + timeoutMs;
|
|
279
|
-
let fresh = [], lastChange = 0, sawOnlyUsed = false;
|
|
335
|
+
let fresh = [], lastChange = 0, sawOnlyUsed = false, idleSince = 0;
|
|
280
336
|
while (Date.now() < deadline) {
|
|
281
337
|
const all = (await fileIds(page)).filter((id) => !before.includes(id));
|
|
282
338
|
const now = all.filter((id) => !used.has(id));
|
|
@@ -287,12 +343,25 @@ async function waitForNewImage(page, before, timeoutMs, used = new Set()) {
|
|
|
287
343
|
if (err) throw new Error(`ChatGPT 가 생성을 거부했습니다: ${err}`);
|
|
288
344
|
}
|
|
289
345
|
if (now.length !== fresh.length) { fresh = now; lastChange = Date.now(); }
|
|
290
|
-
|
|
346
|
+
|
|
347
|
+
// 턴이 끝났는지를 **먼저** 본다. 12초 settle 은 stop 버튼을 못 읽을 때의 보조 장치다.
|
|
348
|
+
const streaming = await isStreaming(page);
|
|
349
|
+
if (streaming) idleSince = 0;
|
|
350
|
+
else if (!idleSince) idleSince = Date.now();
|
|
351
|
+
|
|
352
|
+
if (fresh.length && (idleSince || Date.now() - lastChange >= SETTLE_MS)) {
|
|
291
353
|
if (fresh.length > 1) {
|
|
292
354
|
console.warn(` (경고) 한 프롬프트에 ${fresh.length}장이 생성됨 — 첫 장만 사용합니다.`);
|
|
293
355
|
}
|
|
294
356
|
return fresh[0];
|
|
295
357
|
}
|
|
358
|
+
// 응답은 끝났는데 새 이미지가 없다 = 이 프롬프트는 이미지를 못 받았다.
|
|
359
|
+
// 예전에는 여기서 5분 타임아웃까지 기다렸다가, 남아 있던 앞 장 이미지를 집어갔다.
|
|
360
|
+
if (!fresh.length && idleSince && Date.now() - idleSince >= IDLE_GRACE_MS) {
|
|
361
|
+
throw new Error(sawOnlyUsed
|
|
362
|
+
? '새 이미지가 안 나왔습니다 — 화면에 있는 건 앞 장에 이미 쓴 것입니다. 앞 장을 복사해 넣지 않고 이 장은 실패로 둡니다'
|
|
363
|
+
: 'ChatGPT 가 응답을 끝냈는데 이미지가 없습니다 (글로만 답했을 수 있습니다)');
|
|
364
|
+
}
|
|
296
365
|
await new Promise((r) => setTimeout(r, 3000));
|
|
297
366
|
}
|
|
298
367
|
if (fresh.length) return fresh[0]; // 타임아웃이어도 받은 게 있으면 쓴다
|
|
@@ -519,7 +588,13 @@ async function main() {
|
|
|
519
588
|
console.log('\n=== SUMMARY ===');
|
|
520
589
|
summary.forEach(([f, st, dt, info]) => console.log(` ${f.padEnd(12)} ${st.padEnd(5)} ${dt.padStart(7)} ${info}`));
|
|
521
590
|
const missing = summary.filter(([, st]) => st === 'ERR').length;
|
|
522
|
-
|
|
591
|
+
// ChatGPT 가 실제로 만든 **서로 다른** 이미지 수를 장수와 나란히 찍는다.
|
|
592
|
+
// 같은 이미지를 두 장에 넣으면 파일 수는 맞아도 이 줄에서 어긋난다.
|
|
593
|
+
const ok = summary.filter(([, st]) => st === 'OK').length;
|
|
594
|
+
console.log(`\n생성된 이미지 ${usedIds.size}장 / 저장 ${ok}장 / 요청 ${slides.length}장`);
|
|
595
|
+
if (usedIds.size < ok) console.warn(' (경고) 같은 이미지가 두 장에 들어갔을 수 있습니다.');
|
|
596
|
+
logDom('생성 결과', { made: usedIds.size, saved: ok, want: slides.length, err: missing });
|
|
597
|
+
console.log(`출력: ${outDir}`);
|
|
523
598
|
console.log(missing
|
|
524
599
|
? `Chrome 창은 열어둡니다 (${missing}장 남음). 같은 명령을 다시 부르면 그 창에 붙어 이어서 뽑습니다.`
|
|
525
600
|
: 'Chrome 창은 열어둡니다. 다음 실행이 같은 창에 붙습니다.');
|