Files
koriweb 2174504b59 feat: v2.2.162-168 — /stocks analysis 6차원 확장 + position + /youtube info 재설계
v2.2.162-163: 신규 /stocks analysis <심볼> (펀더멘털 + 1년 차트 + LLM 종합).
  - 6차원: 가치/수익성/안정성(부채비율)/추세(MA 정배열+224회복)/안전마진/RSI 진입 타이밍
  - 신규 /stocks position [심볼] <총자산> <리스크%> <손절%> — 포지션 사이징 계산기

v2.2.164-165: /youtube info 3-tier 재설계 (사용자 피드백: 중복·이모지·표 깨짐).
  - 9개 섹션 → 4개 ## 섹션 (30초 요약 / 핵심 개념 / 깊이 분석 / 정리자 노트)
  - 헤더 이모지 전면 제거, 표 → bullet, 한 줄 요약 중복 제거

v2.2.166: /stocks analysis 매매 타점 신규 섹션 (사용자 매매 규칙 raw 데이터 적응).
  - 매수 진입(3순위 시나리오) / 손절 / 익절 / 관망 해제 트리거
  - LLM이 실제 가격(MA값, 1년 고가, 60일 저점) 자동 채움

v2.2.167: /stocks analysis 분석 로직 정밀화 (사용자 피드백 5건).
  - MA224 3-state (passed/failed/notApplicable) — 추세 확립 종목  오해 차단
  - 낙폭과대 failReason 명시 — 인과 거꾸로 해석 차단
  - 우선주(끝자리 5/7/9) 자동 감지 → 보통주 현재가 fetch → 할인율 계산
  - 프롬프트 판단 절제 규칙 4건 (PBR 절대값 단정/거래량 미세변동/우선주 특이/오탈자)

v2.2.168: 재패키징 (별개 Datacollect bridge 수정과 함께 깨끗한 설치본).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 14:59:34 +09:00

348 lines
18 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* `/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<number, string[]>();
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<number, string[]>();
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:ssmm: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]
\`\`\`
> ⚠️ 본 분석은 스크립트의 언어·구조 패턴 학습용입니다. 대사·자료는 직접 창작/라이선스 확보.`;
}