/** * `/youtube` slash command 의 LLM 입력 빌더 + 자막 변환 헬퍼. * - formatHms / fullScriptFromSegments / bucketSegments — segment list 가공 * - YoutubeAnalysisMode — info/benchmark/both 라우팅 enum (slashRouter 가 사용) * - buildInfoExtractionPrompt — *영상 내용(지식)* 카드 추출 프롬프트 * - build4LensPrompt — 영상 *제작 기법* (훅/구조/제작/CTR) 4-렌즈 분석 프롬프트 * * 옛 코드: slashRouter.ts 의 320줄짜리 inline 블록. 분리해 (a) 두 프롬프트가 같은 * segment 변환 helper 를 자연스럽게 공유, (b) 새 모드 추가 시 한 파일만 수정, * (c) 단위 테스트로 prompt 회귀 확인 가능. */ export function formatHms(totalSec: number): string { if (!isFinite(totalSec) || totalSec <= 0) return '00:00'; const s = Math.floor(totalSec); const h = Math.floor(s / 3600); const m = Math.floor((s % 3600) / 60); const sec = s % 60; return h > 0 ? `${h}:${String(m).padStart(2, '0')}:${String(sec).padStart(2, '0')}` : `${m}:${String(sec).padStart(2, '0')}`; } /** * 영상 전체 자막을 30초 버킷으로 묶어 `[mm:ss] 문장…` 형태의 읽기 좋은 full script로 변환. * YouTube 자동자막은 segment가 잘게 끊겨 그대로 나열하면 가독성이 나쁘므로 묶는다. */ export function fullScriptFromSegments(segments: any[] | undefined): string { if (!segments || segments.length === 0) return '(자막 없음 — 자동 자막이 없는 영상일 수 있습니다.)'; const buckets = new Map(); for (const seg of segments) { const b = Math.floor((seg.start || 0) / 30); const arr = buckets.get(b) || []; arr.push(String(seg.text || '').trim()); buckets.set(b, arr); } return Array.from(buckets.entries()) .sort((a, b) => a[0] - b[0]) .map(([b, texts]) => `**[${formatHms(b * 30)}]** ${texts.join(' ').replace(/\s+/g, ' ').trim()}`) .join('\n\n'); } /** * timestamped segments → 분 단위 버킷으로 묶은 "타임라인 뼈대" 텍스트. * §2 구조 분석에서 LLM 토큰을 아낀다. */ export function bucketSegments(segments: any[] | undefined, bucketSec = 30): { time: string; text: string }[] { if (!segments || segments.length === 0) return []; const buckets = new Map(); for (const seg of segments) { const bucket = Math.floor(seg.start / bucketSec); const arr = buckets.get(bucket) || []; arr.push(String(seg.text || '').trim()); buckets.set(bucket, arr); } return Array.from(buckets.entries()) .sort((a, b) => a[0] - b[0]) .map(([bucket, texts]) => ({ time: formatHms(bucket * bucketSec), text: texts.join(' ').replace(/\s+/g, ' ').trim().slice(0, 240), })); } /** Astra `/youtube` 의 분석 모드. 사용자 입력 `mode:info|benchmark|both`. */ export type YoutubeAnalysisMode = 'info' | 'benchmark' | 'both'; /** * 정보 추출(info) 모드 LLM 프롬프트 — 영상의 *내용·지식* 자체를 다룬다. * * 의도: build4LensPrompt 가 "이 영상을 어떻게 베껴 만들지" 의 벤치마킹 톤이라 * 튜토리얼·강의·뉴스·인터뷰·리뷰 같은 정보형 영상에서는 가치가 낮다. 이 함수는 * 정반대 방향 — 영상이 *말한 것* 을 사실·주장·근거 단위로 추출해서, 사용자가 * 영상을 안 다시 봐도 의사결정·학습·인용에 바로 쓸 수 있는 지식 카드로 정리한다. * * 출력 규칙은 build4LensPrompt 와 일관 (마크다운, 한국어, 자막에 있는 것만 인용). */ export function buildInfoExtractionPrompt(video: any, userContent: string): string { const meta = video.metadata || {}; const segments = video.segments || []; // 자막 본문 — info 모드는 *전체* 본문을 보여줘야 사실 추출이 정확. 단, // LLM 컨텍스트 한도 고려해 너무 길면 trim. 12000자 = 가벼운 강의 60분 분량 정도. const fullText = segments.map((s: any) => String(s.text || '').trim()).join(' ').replace(/\s+/g, ' '); const trimmed = fullText.length > 12000 ? fullText.slice(0, 12000) + ' …[자막 일부 잘림]' : fullText; const slim = { url: meta.webpage_url || `https://www.youtube.com/watch?v=${video.video_id}`, title: meta.title || video.title, channel: meta.channel, durationSec: meta.duration, durationHms: meta.duration_string, uploadDate: meta.upload_date, viewCount: meta.view_count, likeCount: meta.like_count, tags: (meta.tags || []).slice(0, 8), categories: meta.categories, chapters: meta.chapters, descriptionPreview: (meta.description || '').slice(0, 600), }; const today = new Date().toISOString().slice(0, 10); const userBlock = userContent.trim() ? `\n\n[사용자 컨텍스트 — 사용자가 어떤 관점에서 이 영상을 활용하려는지]\n${userContent.trim()}` : ''; return `당신은 영상 콘텐츠를 *지식 카드*로 변환하는 정보 큐레이터입니다. 사장님이 이 영상을 다시 보지 않고도 핵심 정보를 그대로 활용할 수 있도록, 영상이 *말한 것* (주장·사실·근거·결론)을 구조화해서 정리하세요. [분석 원칙 — 모두 반드시 준수] 1. **출처 분리** — 영상 본문(자막)에 *명시된 것* 만 핵심 섹션에 넣음. 정리자의 추론·외부 지식·자기 해석은 별도 \`## 🧩 정리자 노트\` 섹션에만. 두 줄 섞지 말 것. 2. **빈 곳 채우지 말 것** — 자막에 없는 사실은 "본문에 명시되지 않음" 또는 "해당 사례 없음". 3. **신뢰도 라벨 필수** — 모든 핵심 주장 앞에 다음 중 하나: - \`[근거 명시]\` 구체 출처·수치·인용이 본문에 있음 - \`[화자 주장]\` 출처 없는 단정 (디노가 그렇게 말함) - \`[가정]\` 조건부·"~인 것 같다" 표현 - \`[정리자 추론]\` 본문에 없지만 정리자가 추가 (이건 정리자 노트 섹션 전용) 4. **타임스탬프 필수** — 본문 인용·구간 요약·발언 따옴표는 끝에 \`(mm:ss)\` 무조건 붙임. 이걸 빠뜨리면 fail. "(시점 미상)" 도 허용 안 함 — 모르면 인용 자체 빼기. 5. **화자 한 줄 비유 보존 + 방향 보존** — 영상에 비유·은유·"X 는 Y 같은 것" 식 압축 표현이 있으면 반드시 별도 섹션 \`## 💡 화자 한 줄 비유\` 에 보존. 영상의 결정적 요약이 거기 들어 있을 가능성 큼. 없으면 "본문에 명시된 한 줄 비유 없음" 명시. ⚠️ **비유는 방향이 뒤집히기 쉬움** — 화자가 "Hugging Face = 자료실, Reddit = 공부방" 이라 했으면 정확히 그 짝(어느 쪽이 자료실이고 어느 쪽이 공부방인지)을 그대로 따옴표 인용으로 보존. 정리자가 단어 위치를 바꾸거나 뜻을 의역하면 안 됨. 고유명사·수치· 대응 관계도 마찬가지 — 본문 그대로. 6. **순서·단계 발명 금지** — 화자가 "A → B → C 순서로" 라고 명시하지 *않았으면* "단계적 학습 순서" 같은 흐름을 정리자가 만들지 말 것. 굳이 필요하면 정리자 노트로. 7. 한국어 마크다운. **헤더에 이모지 금지** — 스캔할 때 시선이 분산되어 내용이 가려진다. 8. **표 사용 신중** — 모든 셀이 명확한 값으로 채워질 확신이 없으면 표 만들지 말고 bullet로. 깨진 셀(\`way\` 같은 LLM artifact)·빈 셀·"…" placeholder 절대 노출 금지. 9. **중복 금지** — 같은 내용은 한 곳에만. "한 줄 요약"이 여러 섹션에 반복되면 안 됨. 30초 요약에 한 번, 끝. [영상 메타데이터] \`\`\`json ${JSON.stringify(slim, null, 2)} \`\`\` [자막 본문] ${trimmed}${userBlock} [필수 출력 형식 — 정확히 이 구조. 아래 4개 ## 섹션 외 추가 금지. 3-tier 깊이: ① 30초 요약(skim) → ② 핵심 개념(이해) → ③ 깊이 분석(deep dive). 읽는 사람이 목적에 따라 멈출 수 있게.] # ${slim.title || video.title} — 정보 추출 카드 > **영상 URL**: ${slim.url} · **분석 일자**: ${today} · **길이**: ${slim.durationHms || (slim.durationSec ? formatHms(slim.durationSec) : '?')} · **채널**: ${slim.channel || '?'} > 신뢰도 라벨: \`[근거 명시]\` 본문 출처/수치 · \`[화자 주장]\` 출처 없는 단정 · \`[가정]\` 조건부 · \`[정리자 추론]\` 정리자 노트 전용 --- ## 30초 요약 *바쁜 사람은 여기서 멈춰도 영상을 안 본 것보다 낫게.* - **한 줄**: 영상 전체를 한 문장으로 — "무엇이 누구에게 왜 중요한가". 화자 표현 기준, 정리자 의역 금지. - **핵심 포인트 3개**: 영상이 *명시한* 결론·주장만 (정리자 추론 금지). 각 항목 신뢰도 라벨 + 한 줄 + 타임스탬프. 1. **[근거 명시]** ... — (mm:ss) 2. **[화자 주장]** ... — (mm:ss) 3. ... — (mm:ss) - **화자 한 줄 비유** (있는 경우만): 화자가 *전체 메시지를 한 줄로 압축한 비유·은유* 그대로 따옴표. ⚠️ 방향 보존 필수 — "Hugging Face = 자료실, Reddit = 공부방" 같으면 짝과 순서를 본문 그대로. 정리자가 단어 위치를 바꾸거나 의역하면 안 됨. 고유명사·수치·대응관계도 본문 그대로. 예: "..." — (mm:ss). 본문에 비유 없으면 이 bullet 통째로 생략(line drop). --- ## 핵심 개념 설명 *개념을 이해하고 싶은 사람을 위해.* 영상이 다룬 *주요 개념·용어* 2-5개. 각각 정의 + 영상에서의 등장 맥락. 영상이 개념 위주가 아니라 사례·잡담·일화 위주면 이 ## 섹션 통째로 한 줄로 갈음: "이 영상은 개념보다 사례·주장 위주 — 핵심은 30초 요약 참조." ### {개념 이름 1} - **정의**: 화자가 어떻게 정의했는지 (또는 일반 정의 + 화자 사용 방식). - **영상에서**: "직접 인용" — (mm:ss). 등장 맥락 1-2문장. ### {개념 이름 2} - **정의**: ... - **영상에서**: ... --- ## 깊이 분석 *깊게 보고 싶은 사람을 위한 추가 자료.* ### 타임라인 영상 30분 이상이면 chapters(메타데이터에 있으면 사용) 또는 흐름 단위로 4-7개 구간 압축. 30분 미만이면 이 sub-section 통째로 한 줄: "영상이 짧아 생략." - **[mm:ss–mm:ss]** 구간 핵심 한 문장 - ... ### 결정적 발언 (인용용) 글·발표·메모에 그대로 복붙 가능한 *한 문장 인용* 3-5개. 타임스탬프 필수. 위 30초 요약의 핵심 포인트와 중복되는 인용은 빼고, 보조 발언 위주로. - "직접 인용 한 문장" — ${slim.channel || '?'} (mm:ss) - ... ### 더 파고들 질문 영상이 답하지 않았거나 추가 검증 필요한 사항 2-4개. 사장님이 다음 자료를 찾을 때 검색어로 쓸 수 있게 구체적으로. - "본문에서 X 가 Y 라고 했지만 Z 데이터 출처는 명시 안 됨 — 원 데이터 찾아볼 것" - ... ### 구체 수치·데이터 **표 만들지 말 것** — bullet로, *완성된 정보만*. 본문에 모호하거나 정리자 추측이 들어가야 할 데이터는 통째로 생략. 항목: \`{이름}: {값} (mm:ss)\` 형태. - ... (본문에 명시된 구체 수치 없으면 이 sub-section 통째로 한 줄: "본문에 명시된 구체 수치 없음.") --- ## 정리자 노트 (선택) *본문에 없지만* 정리자가 추가로 짚고 싶은 맥락·해석·연결·경고. 모두 \`[정리자 추론]\` 라벨로 시작 — 독자가 "이건 화자가 말한 게 아니라 LLM이 추론한 것"으로 즉시 식별. - **[정리자 추론]** ... - ... 보강할 게 없으면 이 ## 섹션 통째로 한 줄: "정리자 추가 노트 없음 — 본문 그대로가 명확함."`; } /** * extract된 영상 → 유튜브 4-렌즈(훅/구조/제작/CTR) 분석 LLM 프롬프트. * Datacollect 웹앱(YoutubePanel)의 build4LensPrompt를 그대로 이식. */ export function build4LensPrompt(video: any, userContent: string): string { const meta = video.metadata || {}; const segments = video.segments || []; // 초반 30초 / 60초 텍스트 — §1 훅 분석용. const first30s = segments.filter((s: any) => s.start < 30).map((s: any) => String(s.text || '').trim()).join(' ').slice(0, 600); const first60s = segments.filter((s: any) => s.start < 60).map((s: any) => String(s.text || '').trim()).join(' ').slice(0, 1200); // 타임라인 버킷 (30초 단위) — §2 구조 분석용. const timelineBuckets = bucketSegments(segments, 30); const timelinePreview = timelineBuckets.slice(0, 24).map(b => `[${b.time}] ${b.text}`).join('\n'); // 인게이지먼트 키워드 매치 — §2 보조. const engagementHits = segments .filter((s: any) => /구독|좋아요|알림|댓글|공유|subscribe|like|comment/i.test(String(s.text || ''))) .slice(0, 5) .map((s: any) => ({ t: formatHms(s.start), text: String(s.text || '').trim().slice(0, 100) })); const slim = { url: meta.webpage_url || `https://www.youtube.com/watch?v=${video.video_id}`, title: meta.title || video.title, channel: meta.channel, durationSec: meta.duration, durationHms: meta.duration_string, viewCount: meta.view_count, likeCount: meta.like_count, commentCount: meta.comment_count, uploadDate: meta.upload_date, thumbnail: meta.thumbnail, tags: (meta.tags || []).slice(0, 12), categories: meta.categories, chapters: meta.chapters, descriptionPreview: (meta.description || '').slice(0, 600), opening30s: first30s, opening60s: first60s, engagementMoments: engagementHits, segmentCount: segments.length, timelinePreview, }; const today = new Date().toISOString().slice(0, 10); const userBlock = userContent.trim() ? userContent.trim() : '(미입력 — 일반 콘텐츠 제작자 컨텍스트로 작성)'; return `당신은 유튜브 '대본(스크립트)' 분석 전문가이자 콘텐츠 작가입니다. 사장님이 이 영상과 비슷한 콘텐츠의 **대본을 직접 쓰려** 합니다. 영상 연출이 아니라 오직 스크립트(텍스트)와 언어 구조만 분석해, 읽자마자 자기 대본에 복붙하듯 써먹을 수 있는 '유저 친화적 역기획서'를 작성하세요. [분석 원칙] 1. BGM·자막·컷 전환·썸네일 등 대본만으로 알 수 없는 '영상 연출' 항목은 과감히 생략한다. 오직 스크립트(텍스트)와 언어 구조에만 집중한다. 2. 대사를 단순 인용하지 말고, 그 대사가 시청자 심리를 어떻게 건드렸는지 '언어적 장치'를 태그로 라벨링한다. 아래 태그 어휘에서만 골라 일관되게 사용한다: #FOMO #권위부여 #호기심갭 #사회적증명 #페르소나 #약속Promise #공감후킹 #반전 #숫자강조 #문제고발 #브릿지멘트 #쉬운비유 3. 전문 용어가 나오면, 화자가 그것을 어떤 '쉬운 비유'나 일상어로 풀어 말했는지 그 구어체 '말의 맛'을 반드시 분석에 포함한다. 4. 한국어. 자막(text)·chapters·메타데이터에 있는 것만 인용(추측 금지). 타임스탬프는 mm:ss. [영상 데이터] \`\`\`json ${JSON.stringify(slim, null, 2)} \`\`\` [우리가 만들고 싶은 콘텐츠 / 채널 컨텍스트] ${userBlock} [필수 출력 형식 — 정확히 이 구조. 아래 5개 섹션 외 추가 금지] # ${slim.title || video.title} — 대본 역기획서 > **영상 URL**: ${slim.url} · **분석 일자**: ${today} · **길이**: ${slim.durationHms || (slim.durationSec ? formatHms(slim.durationSec) : '?')} · **채널**: ${slim.channel || '?'} ## 🎬 한 줄 인상 (One-line Read) (이 영상 스크립트의 핵심 성격과 설득 전략을 한 줄로. 예: "전문 지식을 친구에게 설명하듯 풀어내고, 호기심 갭으로 끝까지 끌고 가는 정보형 대본") ## 1. 스크립트 뼈대 구조도 (Script Architecture) 구간별 마크다운 표 1개. '레퍼런스 실제 대사'는 자막에서 1문장 이내로 짧게 따옴표 인용. '스크립트 기능'에는 위 태그 어휘를 1~2개 붙인다. 비중 %는 durationSec 기준, chapters가 있으면 그것을, 없으면 timelinePreview로 구간을 추정. | 구간 (비중) | 스크립트 기능 (태그) | 레퍼런스 실제 대사 | 벤치마킹 핵심 기술 | | --- | --- | --- | --- | | 오프닝 Hook (0:00~?, ?%) | #호기심갭 #약속Promise | "첫 대사…" | 결과를 미리 흘려 이탈 차단 | | 도입부 (?~?, ?%) | … | … | … | | 본론 (?~?, ?%) | … | … | … | | 아웃트로·CTA (?~?, ?%) | … | … | … | ## 2. 말의 맛 & 톤앤매너 (Tone & Manner) - **문장 길이 특징**: 단문/장문, 호흡, 리듬 — 실제 자막 예시 1개를 따옴표로. - **어조 페르소나**: 예) 친근한 전문가체 / 단정적 신뢰체 — 근거 대사 1개. - **핵심 대사 장치**: 시청자 중간 이탈을 막으려 대본 사이에 심은 미끼 문장·브릿지 멘트를 타임스탬프와 함께 2~3개 추출, 각각 태그 라벨을 붙인다. - **전문용어 → 쉬운 비유**: 어려운 개념을 화자가 어떤 비유·일상어로 풀었는지 \`용어 → "화자의 실제 표현"\` 형태로 2~3개. 사례가 없으면 "해당 사례 없음"이라 명시. ## 3. 내 대본에 바로 쓰는 액션 체크리스트 (Action Items) 다음 대본을 쓸 때 무조건 적용할 행동 지침 3~4개. 반드시 체크박스로, 구체적 수치를 포함. - [ ] (예: 오프닝 15초 안에 '내가 누구인지' 페르소나 한 문장 박기) - [ ] … - [ ] … ## ✂️ 빈칸 채우기식 대본 템플릿 (Fill-in-the-Blank) 레퍼런스의 말하기 구조·접속사·리듬은 그대로 살리고, 내 콘텐츠 내용만 [ ]에 채우면 대본이 완성되는 형태. 각 [ ] 안에는 무엇을 넣을지 짧은 힌트를 적는다. \`\`\` [오프닝 — Hook] "여러분, 혹시 [시청자의 흔한 고민]… 해보신 적 있으세요? 오늘은 [이 영상이 줄 핵심 결과]를 [숫자]분 만에 끝내 드릴게요." [도입부 — 공감 + 권위] … [본론 — 단계별 설명] … [아웃트로 — CTA] … \`\`\` > ⚠️ 본 분석은 스크립트의 언어·구조 패턴 학습용입니다. 대사·자료는 직접 창작/라이선스 확보.`; }