sns-auto-builder 1.0.8 → 1.0.9

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/index.html CHANGED
@@ -722,7 +722,7 @@ function renderIg({ dir, slides }) {
722
722
  sync();
723
723
  }
724
724
 
725
- function renderSlides({ dir, slides, canRegen }) {
725
+ function renderSlides({ dir, slides, canRegen, expected }) {
726
726
  const host = $('slides');
727
727
  renderIg({ dir, slides });
728
728
  host.textContent = '';
@@ -744,8 +744,21 @@ function renderSlides({ dir, slides, canRegen }) {
744
744
  if (canRegen) cell.append(regenButton({ dir, f, img, cell }));
745
745
  host.append(cell);
746
746
  });
747
- $('outPath').textContent = `${slides.length}장 · ${dir}/ 슬라이드를 누르면 내려받습니다`
748
- + (canRegen ? ' · 오타가 있으면 “다시 만들기”' : '');
747
+ // 장수가 모자란 채로 "완료"만 뜨면, 번이 빠졌는지 모르고 그대로 쓰게 된다.
748
+ // 실제로 ChatGPT 3장만 만들었는데 4장인 것처럼 끝난 적이 있다.
749
+ const want = Number(expected) || 0;
750
+ const short = want && slides.length < want;
751
+ if (short) {
752
+ const got = new Set(slides.map((f) => Number(f.match(/\d+/)[0])));
753
+ const miss = [];
754
+ for (let i = 1; i <= want; i++) if (!got.has(i)) miss.push(i);
755
+ $('outPath').innerHTML = `<b style="color:#e0603a">${want}장 중 ${slides.length}장만 나왔습니다`
756
+ + ` — 빠진 장: ${miss.join(', ')}번</b><br>ChatGPT 가 그 장의 이미지를 안 준 것입니다.`
757
+ + ` 아래에서 다시 만들거나, 같은 주제로 한 번 더 실행하세요.`;
758
+ } else {
759
+ $('outPath').textContent = `${slides.length}장 · ${dir}/ — 슬라이드를 누르면 내려받습니다`
760
+ + (canRegen ? ' · 오타가 있으면 “다시 만들기”' : '');
761
+ }
749
762
  $('outPath').hidden = false;
750
763
  // 한 장씩 누르면 브라우저가 연속 다운로드를 막는다. zip 하나로 묶어 받는다.
751
764
  const zip = $('zipAll');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sns-auto-builder",
3
- "version": "1.0.8",
3
+ "version": "1.0.9",
4
4
  "description": "인스타 카드뉴스 자동 제작 빌더. Claude Code + ChatGPT 웹을 그대로 써서 API 키 없이 돌아갑니다.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -270,11 +270,17 @@ const readPageError = (page) => page.evaluate((needles) => {
270
270
  return (line || hit).trim().slice(0, 200);
271
271
  }, CHATGPT_ERRORS);
272
272
 
273
- async function waitForNewImage(page, before, timeoutMs) {
273
+ // used = 이 실행에서 **이미 저장에 쓴** 이미지들. before(이번 턴 직전 스냅샷)만으로는
274
+ // 부족하다: ChatGPT 가 위쪽 메시지를 DOM 에서 걷어냈다가 다시 붙이면 그 이미지가
275
+ // before 에 없어서 "새 것" 으로 잡힌다. 실제로 그렇게 2번 이미지가 3번 자리에도
276
+ // 저장돼, 3장만 만들어졌는데 4장이 나온 것처럼 끝난 적이 있다.
277
+ async function waitForNewImage(page, before, timeoutMs, used = new Set()) {
274
278
  const deadline = Date.now() + timeoutMs;
275
- let fresh = [], lastChange = 0;
279
+ let fresh = [], lastChange = 0, sawOnlyUsed = false;
276
280
  while (Date.now() < deadline) {
277
- const now = (await fileIds(page)).filter((id) => !before.includes(id));
281
+ const all = (await fileIds(page)).filter((id) => !before.includes(id));
282
+ const now = all.filter((id) => !used.has(id));
283
+ if (all.length && !now.length) sawOnlyUsed = true;
278
284
  if (!now.length) {
279
285
  // 이미지가 아직 없을 때만 본다. 이미 나왔으면 옛 대화의 문구를 오탐할 수 있다.
280
286
  const err = await readPageError(page);
@@ -290,6 +296,10 @@ async function waitForNewImage(page, before, timeoutMs) {
290
296
  await new Promise((r) => setTimeout(r, 3000));
291
297
  }
292
298
  if (fresh.length) return fresh[0]; // 타임아웃이어도 받은 게 있으면 쓴다
299
+ if (sawOnlyUsed) {
300
+ throw new Error('이 프롬프트로는 새 이미지가 안 나왔습니다 (화면에 있는 건 앞 장에 이미 쓴 것). '
301
+ + '앞 장을 복사해 넣으면 안 되므로 이 장은 실패로 둡니다');
302
+ }
293
303
  throw new Error(`이미지가 ${Math.round(timeoutMs / 1000)}초 안에 안 나옴`);
294
304
  }
295
305
 
@@ -444,6 +454,7 @@ async function main() {
444
454
 
445
455
  const summary = [];
446
456
  let primed = false; // 이 실행에서 스타일/레퍼런스를 실은 첫 장을 이미 보냈는가
457
+ const usedIds = new Set(); // 이미 저장에 쓴 이미지 — 같은 것을 두 장에 넣지 않기 위해
447
458
  const page = await browser.newPage();
448
459
  try {
449
460
  await page.goto(CHATGPT, { waitUntil: 'domcontentloaded', timeout: 120_000 });
@@ -479,7 +490,8 @@ async function main() {
479
490
  ).catch(() => console.warn('\n (경고) 레퍼런스 썸네일을 확인하지 못했습니다.'));
480
491
  before = await fileIds(page);
481
492
  }
482
- const fileId = await waitForNewImage(page, before, timeoutMs);
493
+ const fileId = await waitForNewImage(page, before, timeoutMs, usedIds);
494
+ usedIds.add(fileId);
483
495
  let buf = await downloadImage(page, fileId);
484
496
  const src = checkRatio(buf);
485
497
  if (mode === 'slide') buf = await cropTo45(buf, cropPage, logo);
package/server.mjs CHANGED
@@ -887,7 +887,10 @@ ${context || '(없음)'}
887
887
  // 이미지 트랙만 장 단위 재생성이 된다. HTML 트랙은 글자를 HTML 이 그리므로 오타가 안 난다.
888
888
  const meta = { engine, refs: paths, slides: engine === 'image' ? newestPromptFile() : null, count: slideCount, topic };
889
889
  writeGenMeta(abs, meta);
890
- send({ result: { dir, slides, caption, canRegen: !!meta.slides } });
890
+ // expected 같이 보낸다. 장수가 모자라도 화면은 "완료"만 띄웠고,
891
+ // 몇 번이 빠졌는지는 아무 데도 안 나왔다.
892
+ if (slides.length < slideCount) log('장수 부족', { dir, got: slides.length, want: slideCount });
893
+ send({ result: { dir, slides, caption, canRegen: !!meta.slides, expected: slideCount } });
891
894
  });
892
895
  // 모델 출력을 기다리지 않고 바로 감시를 켠다. 완성된 슬라이드가 생기는 즉시 화면에 뜬다.
893
896
  stopWatch = watchProgress(dir, engine, sendOut, slideCount);