Junhoee commited on
Commit
4105b78
Β·
verified Β·
1 Parent(s): f8d9250

Upload 140 files

Browse files
content/beatcards/E06.yaml CHANGED
@@ -410,8 +410,6 @@ cards:
410
  text: 그리고 κ·Έ μˆœκ°„, 맀일 밀의 꿈과 이 두 점이 λ¬΄κ΄€ν•˜μ§€ μ•Šμ„μ§€λ„ λͺ¨λ₯Έλ‹€λŠ” 생각이, 처음으둜 머릿속을 슀치고 μ§€λ‚˜κ°”λ‹€.
411
  - type: prose
412
  text: λ‚˜λŠ” 고개λ₯Ό μ €μ—ˆλ‹€. κ·ΈλŸ¬λ‚˜ ν•œλ²ˆ λ– μ˜€λ₯Έ 그것은, 거울 속 ν‘Έλ₯Έ 두 점처럼, 아무리 외면해도 사라지지 μ•Šμ•˜λ‹€.
413
- - type: prose
414
- text: μˆ¨κΉ€ β†’ **E06-10C** (의쑴 심화, β‘‘β‘’ μ˜¨λž¨ν”„) Β· μ•Œλ¦Ό β†’ **E06-10B** (μ•ˆμ „λ§ μž‘λ™, β‘ μ²˜λ‹¨ μ˜¨λž¨ν”„)
415
  choices:
416
  - label: 두 점을 가리고 혼자 κ°„μ§ν•œλ‹€
417
  modifiers: &id002 []
 
410
  text: 그리고 κ·Έ μˆœκ°„, 맀일 밀의 꿈과 이 두 점이 λ¬΄κ΄€ν•˜μ§€ μ•Šμ„μ§€λ„ λͺ¨λ₯Έλ‹€λŠ” 생각이, 처음으둜 머릿속을 슀치고 μ§€λ‚˜κ°”λ‹€.
411
  - type: prose
412
  text: λ‚˜λŠ” 고개λ₯Ό μ €μ—ˆλ‹€. κ·ΈλŸ¬λ‚˜ ν•œλ²ˆ λ– μ˜€λ₯Έ 그것은, 거울 속 ν‘Έλ₯Έ 두 점처럼, 아무리 외면해도 사라지지 μ•Šμ•˜λ‹€.
 
 
413
  choices:
414
  - label: 두 점을 가리고 혼자 κ°„μ§ν•œλ‹€
415
  modifiers: &id002 []
engine/__pycache__/router.cpython-314.pyc CHANGED
Binary files a/engine/__pycache__/router.cpython-314.pyc and b/engine/__pycache__/router.cpython-314.pyc differ
 
engine/router.py CHANGED
@@ -34,9 +34,8 @@ COVER_IMAGE = "/assets/ν”Œλž˜μ΄ 이미지/1.png"
34
  IMAGE_MAP = {
35
  "E01-07": "/assets/ν”Œλž˜μ΄ 이미지/4.png", # 6μ„Έμ˜ λ°€ β€” "κ·Έλ…€λŠ” 미동도 없이 λ‚˜λ₯Ό λ‚΄λ €λ‹€λ³΄μ•˜λ‹€"
36
  "E03-03": "/assets/인물/μΉ΄λ₯΄λ°€λΌ.png", # 재회 β€” 같은 꿈의 λŒ€λ©΄
37
- "E05-02": "/assets/λ°°κ²½/μ„±.png", # μ΄ˆμƒν™”μ˜ λ°€ 무렡 β€” κ³ μ„±
38
  "E06-09": "/assets/인물/μΉ΄λ₯΄λ°€λΌ κ°€λ©΄.png", # λ‘˜λ§Œμ˜ λΉ„λ°€ β€” κ°€μž₯ μœ„ν—˜ν•œ 닀정함
39
- "E06-10": "/assets/λ°°κ²½/μΉ¨μ‹€.png", # 도달점 β€” 두 점 발견
40
  }
41
 
42
  # 인트둜 μ‹œν€€μŠ€ β€” ν‘œμ§€ "밀을 μ—°λ‹€" λ’€, μƒˆ μ‹œμž‘μ—λ§Œ (이어 μ½κΈ°λŠ” κ±΄λ„ˆλœ€)
 
34
  IMAGE_MAP = {
35
  "E01-07": "/assets/ν”Œλž˜μ΄ 이미지/4.png", # 6μ„Έμ˜ λ°€ β€” "κ·Έλ…€λŠ” 미동도 없이 λ‚˜λ₯Ό λ‚΄λ €λ‹€λ³΄μ•˜λ‹€"
36
  "E03-03": "/assets/인물/μΉ΄λ₯΄λ°€λΌ.png", # 재회 β€” 같은 꿈의 λŒ€λ©΄
 
37
  "E06-09": "/assets/인물/μΉ΄λ₯΄λ°€λΌ κ°€λ©΄.png", # λ‘˜λ§Œμ˜ λΉ„λ°€ β€” κ°€μž₯ μœ„ν—˜ν•œ 닀정함
38
+ # E05-02(μ„±)Β·E06-10(μΉ¨μ‹€) 컷은 제거 β€” μž₯μ†Œ λ°°κ²½κ³Ό 쀑볡이라 μ»· νŽ˜μ΄μ§€κ°€ ꡰ더더기 (μœ μ € κ²°μ •)
39
  }
40
 
41
  # 인트둜 μ‹œν€€μŠ€ β€” ν‘œμ§€ "밀을 μ—°λ‹€" λ’€, μƒˆ μ‹œμž‘μ—λ§Œ (이어 μ½κΈ°λŠ” κ±΄λ„ˆλœ€)
engine/schemas/__pycache__/validate.cpython-314.pyc CHANGED
Binary files a/engine/schemas/__pycache__/validate.cpython-314.pyc and b/engine/schemas/__pycache__/validate.cpython-314.pyc differ
 
engine/schemas/validate.py CHANGED
@@ -119,6 +119,17 @@ def check_links(episodes: list[EpisodeFile], chapter, r_cards) -> tuple[list[str
119
  if not has_out:
120
  warnings.append(f"{c.id}: λ‚˜κ°€λŠ” 링크 μ—†μŒ (챕터 μΆœκ΅¬κ°€ μ•„λ‹ˆλ©΄ 결함)")
121
 
 
 
 
 
 
 
 
 
 
 
 
122
  # λΉˆμ•½ μΉ΄λ“œ β€” ν™”λ©΄ ν•˜λ‚˜μ˜ μ΅œμ†Œ 밀도 미달 (μž‘μ„± κ°€μ΄λ“œ: docs/contract/beatcard_guide.md)
123
  MIN_CARD_CHARS = 200
124
  for c in cards.values():
 
119
  if not has_out:
120
  warnings.append(f"{c.id}: λ‚˜κ°€λŠ” 링크 μ—†μŒ (챕터 μΆœκ΅¬κ°€ μ•„λ‹ˆλ©΄ 결함)")
121
 
122
+ # μ €μž‘ 메타 λˆ„μˆ˜ β€” λ³Όλ“œ μΉ΄λ“œID(**E##-) 같은 νŽΈμ§‘ λ©”λͺ¨κ°€ μ§€λ¬Έ/결과에 λ‚¨μœΌλ©΄ 화면에 λ…ΈμΆœλœλ‹€
123
+ import re as _re
124
+ leak = _re.compile(r"\*\*E\d{2}-|\*\*β–·")
125
+ for c in cards.values():
126
+ for b in c.narration:
127
+ if leak.search(b.text):
128
+ warnings.append(f"{c.id}: 지문에 μ €μž‘ 메타 λˆ„μˆ˜ μ˜μ‹¬ β€” {b.text[:40]}…")
129
+ for ch in c.choices:
130
+ if leak.search(ch.result or ""):
131
+ warnings.append(f"{c.id}: 선택 결과에 μ €μž‘ 메타 λˆ„μˆ˜ μ˜μ‹¬ β€” {ch.result[:40]}…")
132
+
133
  # λΉˆμ•½ μΉ΄λ“œ β€” ν™”λ©΄ ν•˜λ‚˜μ˜ μ΅œμ†Œ 밀도 미달 (μž‘μ„± κ°€μ΄λ“œ: docs/contract/beatcard_guide.md)
134
  MIN_CARD_CHARS = 200
135
  for c in cards.values():
scripts/beatcard_convert.py CHANGED
@@ -40,6 +40,9 @@ RE_RESULT_NEXT = re.compile(r"β†’\s*\*{0,2}([A-Z]\d{2}-\d{2}[A-Z]?)\*{0,2}")
40
  RE_BRANCH_MAP = re.compile(r"([κ°€-힣]+)\s*β†’\s*\*\*([A-Z]\d{2}-\d{2}[A-Z]?)\*\*")
41
  RE_RCARD_REF = re.compile(r"R-[κ°€-힣\w]+")
42
  RE_SECTION_STOP = re.compile(r"^(?:β–Έ\s*λ…ΈνŠΈ:|🎬|πŸ–Ό)")
 
 
 
43
  RE_STAGE = re.compile(r"^###\s*(\d)단계\s*[—–-]\s*(.+)$", re.M)
44
 
45
 
@@ -216,6 +219,8 @@ class EpisodeParser:
216
  continue
217
  if RE_SECTION_STOP.match(s):
218
  break
 
 
219
 
220
  if RE_CHOICE_HEADER.match(s):
221
  flush_prose()
 
40
  RE_BRANCH_MAP = re.compile(r"([κ°€-힣]+)\s*β†’\s*\*\*([A-Z]\d{2}-\d{2}[A-Z]?)\*\*")
41
  RE_RCARD_REF = re.compile(r"R-[κ°€-힣\w]+")
42
  RE_SECTION_STOP = re.compile(r"^(?:β–Έ\s*λ…ΈνŠΈ:|🎬|πŸ–Ό)")
43
+ # μ €μž‘ 메타 라인 β€” μ§€λ¬ΈμœΌλ‘œ μƒˆλ©΄ μ•ˆ λ˜λŠ” λΆ„κΈ° λ§€ν•‘ λ©”λͺ¨ (예: "μˆ¨κΉ€ β†’ **E06-10C** (…)").
44
+ # λ³Όλ“œ μΉ΄λ“œID μ°Έμ‘°κ°€ λ³Έλ¬Έ λŒ€μ‚¬/지문에 λ“±μž₯ν•  일은 μ—†λ‹€
45
+ RE_META_LINE = re.compile(r"\*\*E\d{2}-")
46
  RE_STAGE = re.compile(r"^###\s*(\d)단계\s*[—–-]\s*(.+)$", re.M)
47
 
48
 
 
219
  continue
220
  if RE_SECTION_STOP.match(s):
221
  break
222
+ if RE_META_LINE.search(s): # λΆ„κΈ° λ§€ν•‘ λ©”λͺ¨ β€” μ§€λ¬Έ λˆ„μˆ˜ 차단
223
+ continue
224
 
225
  if RE_CHOICE_HEADER.match(s):
226
  flush_prose()
web/index.html CHANGED
@@ -36,6 +36,9 @@
36
  transition: width .8s ease; box-shadow: 0 0 6px rgba(201,168,106,.5); }
37
  #progress-label { position: fixed; top: 10px; right: 16px; color: var(--gold-dim);
38
  font-size: .74rem; letter-spacing: .22em; opacity: .85; z-index: 40; }
 
 
 
39
 
40
  /* ── λ³Έλ¬Έ β€” ν™”λ©΄ λ‹¨μœ„ 읽기: ν•œ 화면은 뷰포트 μ•ˆμ— λ“€μ–΄κ°„λ‹€ (슀크둀 λŒ€μ‹  νŽ˜μ΄μ§€ λ„˜κΉ€) ── */
41
  #stage { max-width: 680px; margin: 0 auto; padding: 54px 24px 96px;
@@ -64,6 +67,7 @@
64
  .vn-stage { position: relative; margin: 1.2em auto 0; height: min(76vh, 860px);
65
  overflow: hidden; border: 1px solid rgba(201,168,106,.28);
66
  box-shadow: 0 0 80px rgba(0,0,0,.9); }
 
67
  .vn-stage-art { position: absolute; inset: 0; width: 100%; height: 100%;
68
  object-fit: cover; object-position: top center; }
69
  .vn-stage::after { content: ""; position: absolute; inset: 0;
@@ -209,6 +213,7 @@
209
 
210
  <div id="progress-wrap"><div id="progress-bar"></div></div>
211
  <div id="progress-label"></div>
 
212
  <div id="browse-ribbon">μ§€λ‚˜μ˜¨ νŽ˜μ΄μ§€ β€” 였λ₯Έμͺ½μ„ 눌러 μ§€κΈˆμœΌλ‘œ</div>
213
 
214
  <div id="cover">
@@ -343,8 +348,8 @@ function charLines(g) { // μ„œμ‚¬ 속 λ°œν™” β€” 쒌 ν™”μžλͺ… Β·
343
  `<div class="char-line fade"><span class="spk">${esc(name)}</span>β€œ${esc(t)}”</div>`).join('');
344
  }
345
 
346
- function vnStage(g, choices) { // 독립 ν™”λ©΄: μ΄ˆμƒ + μ΄λ¦„ν‘œ + κ·Έ μΊλ¦­ν„°μ˜ λŒ€μ‚¬ (+선택지)만
347
- let h = `<div class="vn-stage fade">` +
348
  `<img class="vn-stage-art" src="${encodeURI(g.portrait)}" alt="${esc(g.spk)}">` +
349
  `<div class="vn-stage-panel"><div class="vn-name">${esc(g.spk)}</div>`;
350
  for (const t of g.lines) h += `<div class="vn-line">β€œ${esc(t)}”</div>`;
@@ -424,8 +429,16 @@ function buildScreens(step, fresh) {
424
 
425
  groups.forEach((g, k) => {
426
  if (k === stageIdx) { // μŠ€ν…Œμ΄μ§€ = 독립 ν™”λ©΄ (이미지+λŒ€μ‚¬λ§Œ)
 
 
 
 
 
 
 
 
427
  flushSeg();
428
- screens.push(vnStage(g, stageIsLast ? choices : ''));
429
  return;
430
  }
431
  const html = g.lines ? charLines(g) : block(g.block);
@@ -454,7 +467,8 @@ function choicesHtml(step) {
454
  function setProgress(p, epLabel) {
455
  if (!p) return;
456
  $('progress-bar').style.width = (p.index / p.total * 100).toFixed(1) + '%';
457
- $('progress-label').textContent = `${epLabel || ''} Β· ${p.index} / ${p.total}`;
 
458
  }
459
 
460
  function showStep(step, fresh) {
 
36
  transition: width .8s ease; box-shadow: 0 0 6px rgba(201,168,106,.5); }
37
  #progress-label { position: fixed; top: 10px; right: 16px; color: var(--gold-dim);
38
  font-size: .74rem; letter-spacing: .22em; opacity: .85; z-index: 40; }
39
+ #chapter-label { position: fixed; top: 10px; left: 50%; transform: translateX(-50%);
40
+ color: var(--gold-dim); font-size: .72rem; letter-spacing: .3em;
41
+ text-indent: .3em; opacity: .8; z-index: 40; white-space: nowrap; }
42
 
43
  /* ── λ³Έλ¬Έ β€” ν™”λ©΄ λ‹¨μœ„ 읽기: ν•œ 화면은 뷰포트 μ•ˆμ— λ“€μ–΄κ°„λ‹€ (슀크둀 λŒ€μ‹  νŽ˜μ΄μ§€ λ„˜κΉ€) ── */
44
  #stage { max-width: 680px; margin: 0 auto; padding: 54px 24px 96px;
 
67
  .vn-stage { position: relative; margin: 1.2em auto 0; height: min(76vh, 860px);
68
  overflow: hidden; border: 1px solid rgba(201,168,106,.28);
69
  box-shadow: 0 0 80px rgba(0,0,0,.9); }
70
+ .vn-stage.with-lead { height: min(62vh, 720px); } /* λ¦¬λ“œμΈ 지문이 μœ„μ— λΆ™λŠ” ν™”λ©΄ */
71
  .vn-stage-art { position: absolute; inset: 0; width: 100%; height: 100%;
72
  object-fit: cover; object-position: top center; }
73
  .vn-stage::after { content: ""; position: absolute; inset: 0;
 
213
 
214
  <div id="progress-wrap"><div id="progress-bar"></div></div>
215
  <div id="progress-label"></div>
216
+ <div id="chapter-label"></div>
217
  <div id="browse-ribbon">μ§€λ‚˜μ˜¨ νŽ˜μ΄μ§€ β€” 였λ₯Έμͺ½μ„ 눌러 μ§€κΈˆμœΌλ‘œ</div>
218
 
219
  <div id="cover">
 
348
  `<div class="char-line fade"><span class="spk">${esc(name)}</span>β€œ${esc(t)}”</div>`).join('');
349
  }
350
 
351
+ function vnStage(g, choices, withLead) { // μ΄ˆμƒ + μ΄λ¦„ν‘œ + κ·Έ μΊλ¦­ν„°μ˜ λŒ€μ‚¬ (+선택지)
352
+ let h = `<div class="vn-stage${withLead ? ' with-lead' : ''} fade">` +
353
  `<img class="vn-stage-art" src="${encodeURI(g.portrait)}" alt="${esc(g.spk)}">` +
354
  `<div class="vn-stage-panel"><div class="vn-name">${esc(g.spk)}</div>`;
355
  for (const t of g.lines) h += `<div class="vn-line">β€œ${esc(t)}”</div>`;
 
429
 
430
  groups.forEach((g, k) => {
431
  if (k === stageIdx) { // μŠ€ν…Œμ΄μ§€ = 독립 ν™”λ©΄ (이미지+λŒ€μ‚¬λ§Œ)
432
+ // 직전 λ¦¬λ“œμΈ(role=lead_in, 짧은 λ„μž… μ§€λ¬Έ)은 같은 ν™”λ©΄μ˜ ν”„λ ˆμž„ "λ°– μœ„μͺ½"에 β€”
433
+ // 혼자 화면을 μ°¨μ§€ν•˜μ§€λ„, 이미지에 κ²ΉμΉ˜μ§€λ„ μ•Šκ²Œ. ν”„λ ˆμž„μ€ with-lead둜 μΆ•μ†Œ
434
+ let leadHtml = '';
435
+ const prev = groups[k - 1];
436
+ if (prev && prev.block && prev.block.role === 'lead_in'
437
+ && seg.length && seg[seg.length - 1].html === block(prev.block)) {
438
+ leadHtml = seg.pop().html;
439
+ }
440
  flushSeg();
441
+ screens.push(leadHtml + vnStage(g, stageIsLast ? choices : '', !!leadHtml));
442
  return;
443
  }
444
  const html = g.lines ? charLines(g) : block(g.block);
 
467
  function setProgress(p, epLabel) {
468
  if (!p) return;
469
  $('progress-bar').style.width = (p.index / p.total * 100).toFixed(1) + '%';
470
+ $('progress-label').textContent = `${p.index} / ${p.total}`;
471
+ $('chapter-label').textContent = epLabel || ''; // ν˜„μž¬ μž₯(θ©±) β€” 상단 쀑앙 μƒμ‹œ ν‘œμ‹œ
472
  }
473
 
474
  function showStep(step, fresh) {