From 6c9e20558c7a45a76d73a1e043a2fa612653b64b Mon Sep 17 00:00:00 2001 From: thxforall <113906780+thxforall@users.noreply.github.com> Date: Thu, 14 May 2026 20:38:43 +0900 Subject: [PATCH 1/3] docs: add content-studio pipeline v2 design spec (#530) Post picker modal, UUID packet ID fix, unified LLM call, channel thumbnails, and Research/Firecrawl removal. Co-Authored-By: Claude Opus 4.6 --- ...05-14-content-studio-pipeline-v2-design.md | 254 ++++++++++++++++++ 1 file changed, 254 insertions(+) create mode 100644 docs/superpowers/specs/2026-05-14-content-studio-pipeline-v2-design.md diff --git a/docs/superpowers/specs/2026-05-14-content-studio-pipeline-v2-design.md b/docs/superpowers/specs/2026-05-14-content-studio-pipeline-v2-design.md new file mode 100644 index 00000000..d3c7b93d --- /dev/null +++ b/docs/superpowers/specs/2026-05-14-content-studio-pipeline-v2-design.md @@ -0,0 +1,254 @@ +# Content Studio Pipeline v2 Design + +Issue: #530 + +## Summary + +Content Studio 파이프라인을 개선하여 post ID 수동 입력 대신 **검색/인기순 선택 → 키워드 추출 → 마케팅 프롬프트 생성 → 3종 채널별 썸네일 자동 생성**까지 하나의 플로우로 완성한다. 동시에 Research/Firecrawl 외부 의존성을 전체 제거한다. + +## Motivation + +- 현재 post ID를 직접 입력하는 UX가 불편하고 오류 유발 (500 에러 포함) +- `packet_${postId}` prefix가 UUID 컬럼에 들어가면서 `invalid input syntax for type uuid` 에러 발생 +- Firecrawl 기반 Research는 post 내부 데이터로 충분히 대체 가능 — 불필요한 외부 의존성 +- 마케팅 콘텐츠(YouTube/Instagram/X) 제작 썸네일 자동화 부재 + +## Approach + +**순차 스텝 파이프라인 (Approach A)**. 각 스텝이 명확히 분리되어 스텝별 에러 복구가 간단하며, `gpt-image-2` 이미지 생성 비용을 어드민이 컨트롤할 수 있다. 기존 `page.tsx`의 순차 플로우와 일관성 유지. + +## Pipeline Flow + +``` +[PostPickerModal] Meilisearch 검색 + 인기순 리스트 → Post 선택 + ↓ +[Packet 생성] crypto.randomUUID() + post 데이터 수집 + ↓ +[통합 LLM 호출] 규칙 키워드 + structured output + → { keywords, imagePrompts (YT/IG feed/IG story), variants (YT/IG/X) } + ↓ +[어드민 프리뷰] 키워드 + 프롬프트 + variant 텍스트 확인/수정 + ↓ +[썸네일 생성] "Generate Thumbnails" 버튼 → Promise.all 4종 병렬 + → gpt-image-2 edit 모드 (post 원본 이미지 reference) + ↓ +[저장] Supabase Storage (content-studio/{packetId}/) + ↓ +[Governance] celebrity risk, disclosure 등 체크 (research 규칙 제외) + ↓ +[어드민 리뷰] 썸네일 + 텍스트 확인/수정 → approve/reject +``` + +## Section 1: Post 선택 Modal + +### Component + +`PostPickerModal` — 새 컴포넌트, `app/admin/content-studio/` 하위에 생성. + +### 구조 + +- 상단: Meilisearch 검색 input (debounce 300ms, 기존 한글 검색 인프라 활용) +- 검색 미입력 시: Supabase에서 `view_count + like_count` 기준 인기순 상위 20개 표시 +- 검색 입력 시: Meilisearch `posts` 인덱스 검색 결과 표시 +- 각 행: 썸네일(40×40) + post title + artist/group name + view_count + like_count +- 행 클릭 시: Modal 닫힘 → 선택된 postId로 Packet 생성 자동 트리거 + +### 데이터 흐름 + +- 인기순 초기 리스트: Supabase 직접 쿼리 (`posts` 테이블, `view_count + like_count` 내림차순, limit 20) +- 검색: 기존 Meilisearch 클라이언트 재사용 + +### 기존 코드 변경 + +- `page.tsx`: postId 텍스트 입력 폼 → "Select Post" 버튼 + PostPickerModal로 교체 +- 수동 ID 입력은 Modal 내 하단에 "Enter ID manually" 링크로 유지 (fallback) + +## Section 2: Packet ID 수정 + +### 버그 수정 + +`packet-builder.ts`: + +- `packetId()`: `packet_${postId}` → `crypto.randomUUID()` +- `variantId()`: `${packetIdValue}_${format}` → `crypto.randomUUID()` +- `post_id` FK가 이미 `content_packets` 테이블에서 unique constraint이므로 중복 방지는 DB가 담당 + +### API + +변경 없음. `POST /api/v1/content/packets { postId }` 그대로 유지. 내부에서 UUID 생성만 변경. + +### DB 호환성 + +- `db.ts`의 `upsertPacket`: `onConflict: "post_id"` 유지 — 같은 post로 재생성하면 기존 packet 업데이트 +- 기존 DB 데이터: `packet_${uuid}` 형식의 기존 레코드는 그대로 두고, 신규만 순수 UUID + +## Section 3: 통합 LLM 호출 + +### 새 스키마 + +`llm-schemas.ts`에 `unifiedContentResponseSchema` 추가: + +```typescript +{ + keywords: string[], + imagePrompts: { + youtube: string, + instagram_feed: string, + instagram_story: string, + }, + variants: { + youtube: { title: string, body: string, hashtags: string[] }, + instagram: { title: string, body: string, hashtags: string[] }, + x: { title: string, body: string, hashtags: string[] }, + } +} +``` + +### 호출 플로우 + +1. `packet-builder.ts`에서 규칙 기반 키워드 추출: brand, artist, group, solution categories를 post 데이터에서 수집 +2. 새 함수 `generateUnifiedContent(packet, ruleKeywords)` — 단일 OpenAI structured output 호출 +3. 입력: packet 전체 데이터 + 규칙 키워드 배열 +4. 출력: 위 스키마. LLM이 규칙 키워드를 포함하되 추가 마케팅 키워드도 생성 + +### 기존 코드 정리 + +- `llm-generation.ts`의 `generateVariantsWithMode()` → 새 `generateUnifiedContent()`로 교체 +- `generateChannelVariants()` (템플릿 기반) → template fallback으로 유지 +- `llm-prompts.ts` → 새 통합 프롬프트로 교체 +- 기존 `contentVariantLLMResponseSchema` → 새 `unifiedContentResponseSchema`로 교체 + +### Template fallback + +- `CONTENT_STUDIO_LLM_ENABLED=false`이면 기존 `generateChannelVariants()` + 빈 keywords/imagePrompts 반환 +- LLM 호출 실패 시에도 동일하게 fallback + +## Section 4: 3종 채널별 썸네일 생성 + +### 채널별 사이즈 + +| 채널 | 포맷 키 | 사이즈 | 용도 | +| --------------- | ----------------- | --------- | ----------- | +| YouTube | `youtube` | 1536×1024 | 썸네일 | +| X (Twitter) | `x` | 1536×1024 | 카드 이미지 | +| Instagram Feed | `instagram_feed` | 1024×1024 | 피드 정사각 | +| Instagram Story | `instagram_story` | 1024×1536 | 스토리 세로 | + +### 생성 플로우 + +1. 통합 LLM 결과에서 채널별 `imagePrompts` 추출 +2. post 원본 이미지 URL을 reference image로 사용 +3. `gpt-image-2` edit 모드로 4종 `Promise.all` 병렬 호출 +4. edit 실패 시 → 순수 generate fallback (reference 없이) +5. 생성된 이미지 → Supabase Storage `content-studio/{packetId}/` 경로에 업로드 +6. 업로드 실패 시 → data URL (base64) fallback + +### 기존 코드 활용 + +- `openai-client.ts`의 `fetchImageBytesFromOpenAIEdit` + `uploadToStorage` 재사용 +- 이미지 모델 기본값: `gpt-image-1` → `gpt-image-2`로 업데이트 (`CONTENT_STUDIO_IMAGE_MODEL` 환경변수) +- 새 함수 `generateChannelThumbnails(packet, imagePrompts)` 추가 + +### API + +새 엔드포인트: `POST /api/v1/content/packets/{id}/generate-thumbnails` + +- 요청: `{ imagePrompts: { youtube, instagram_feed, instagram_story } }` +- 응답: `{ thumbnails: { youtube: url, x: url, instagram_feed: url, instagram_story: url }, failures: [] }` +- YouTube와 X는 동일 사이즈(1536×1024)이므로 imagePrompt는 3개이나 출력은 4개 (X는 youtube prompt 재사용) + +### UI + +- LLM 생성 결과 아래에 "Generate Thumbnails" 버튼 추가 +- 이미지 프롬프트를 어드민이 수정할 수 있는 textarea 3개 (YT, IG Feed, IG Story) +- 생성 완료 후 4종 썸네일 프리뷰 그리드 (2×2) + +## Section 5: Research/Firecrawl 전체 제거 + +### 삭제 대상 파일 (13개) + +| 경로 | 유형 | +| ----------------------------------------------------------- | ------------- | +| `lib/content-studio/research/index.ts` | 배럴 | +| `lib/content-studio/research/firecrawl-client.ts` | Firecrawl API | +| `lib/content-studio/research/domain-policy.ts` | 도메인 정책 | +| `lib/content-studio/research/normalization.ts` | 정규화 | +| `lib/content-studio/research/query-suggestions.ts` | 쿼리 제안 | +| `lib/content-studio/research/recommendations.ts` | 추천 | +| `lib/content-studio/research/service.ts` | 서비스 | +| `lib/content-studio/__tests__/research.test.ts` | 테스트 | +| `app/admin/content-studio/ResearchPanel.tsx` | UI 패널 | +| `app/admin/content-studio/__tests__/ResearchPanel.test.tsx` | UI 테스트 | +| `app/api/v1/content/research/route.ts` | API 라우트 | +| `app/api/v1/content/research/__tests__/route.test.ts` | API 테스트 | +| `research/` 디렉토리 전체 | — | + +### 스키마 정리 (`schemas.ts`) + +삭제 대상: + +- `researchSourceSchema`, `researchInsightSchema`, `researchRecommendationsSchema`, `researchRunSchema`, `runResearchRequestSchema` + 관련 타입 export +- `contentPacketSchema`의 `externalEvidence` optional 필드 +- `contentVariantSchema`의 `researchProvenance`, `claims`, `missingFacts` optional 필드 +- `generateVariantsRequestSchema`의 `researchContext`, `useResearchInCopy` 필드 +- `assetPlanRequestSchema`, `shortFormPlanRequestSchema`의 `researchRun`, `useResearchInCopy` 필드 + +### 코드 참조 정리 + +- `page.tsx`: ResearchPanel import + 관련 state 제거 +- `llm-generation.ts`: `researchContext` 관련 로직 제거 +- `openai-client.ts`: research 파라미터 제거 +- `governance-check.ts`: research 관련 규칙만 제거 + +### Governance 유지 항목 + +- celebrity risk check (artist/group name 기반) +- disclosure flags (AI generated, synthetic media, sponsored, rights risk) +- LLM governance check (`contentGovernanceLLMSchema`) + +## Environment Variables + +| 변수 | 용도 | 기본값 | +| ---------------------------- | ---------------- | ------------- | +| `OPENAI_API_KEY` | OpenAI API 키 | — (필수) | +| `CONTENT_STUDIO_LLM_ENABLED` | LLM 호출 활성화 | `false` | +| `CONTENT_STUDIO_MODEL` | 텍스트 LLM 모델 | `gpt-4.1` | +| `CONTENT_STUDIO_IMAGE_MODEL` | 이미지 생성 모델 | `gpt-image-2` | + +## Out of Scope + +- Grok AI 비디오 파이프라인 +- ai-server ARQ 마이그레이션 + R2 저장소 전환 +- 원클릭 자동 파이프라인 모드 (v2 안정화 후 별도 이슈) + +## File Change Summary + +### 새 파일 + +- `app/admin/content-studio/PostPickerModal.tsx` +- `app/api/v1/content/packets/[id]/generate-thumbnails/route.ts` + +### 수정 파일 + +- `lib/content-studio/packet-builder.ts` — UUID 생성 + 규칙 키워드 추출 +- `lib/content-studio/llm-schemas.ts` — `unifiedContentResponseSchema` 추가, 기존 research 스키마 정리 +- `lib/content-studio/llm-generation.ts` — `generateUnifiedContent()` 교체 +- `lib/content-studio/llm-prompts.ts` — 통합 프롬프트 교체 +- `lib/content-studio/schemas.ts` — research 필드 삭제 +- `lib/content-studio/assets/openai-client.ts` — `generateChannelThumbnails()` 추가, 기본 모델 변경 +- `lib/content-studio/index.ts` — export 정리 +- `app/admin/content-studio/page.tsx` — PostPickerModal 통합, ResearchPanel 제거, 썸네일 UI 추가 +- `app/admin/content-studio/AssetPanel.tsx` — research 참조 제거 +- `app/admin/content-studio/ShortFormPanel.tsx` — research 참조 제거 +- `lib/content-studio/governance-check.ts` — research 규칙 제거 +- `lib/content-studio/__tests__/llm-generation.test.ts` — research 컨텍스트 테스트 정리 +- `lib/content-studio/__tests__/content-studio.test.ts` — 통합 테스트 업데이트 + +### 삭제 파일 + +- `lib/content-studio/research/` 디렉토리 전체 (7개) +- `lib/content-studio/__tests__/research.test.ts` +- `app/admin/content-studio/ResearchPanel.tsx` +- `app/admin/content-studio/__tests__/ResearchPanel.test.tsx` +- `app/api/v1/content/research/route.ts` +- `app/api/v1/content/research/__tests__/route.test.ts` From e715703d3d1d576502e1086cbb67cdcb4ea4a6b1 Mon Sep 17 00:00:00 2001 From: thxforall <113906780+thxforall@users.noreply.github.com> Date: Thu, 14 May 2026 20:42:57 +0900 Subject: [PATCH 2/3] fix(vton): handle Vertex AI RAI filter with user-friendly error Vertex AI returns raiFilteredReason instead of image data when safety filter triggers. Log full response in dev and surface actionable message. Co-Authored-By: Claude Opus 4.6 --- packages/web/app/api/v1/vton/route.ts | 16 +++++++++++++++- packages/web/lib/components/vton/VtonModal.tsx | 6 +++++- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/packages/web/app/api/v1/vton/route.ts b/packages/web/app/api/v1/vton/route.ts index 0107d644..92eeacf1 100644 --- a/packages/web/app/api/v1/vton/route.ts +++ b/packages/web/app/api/v1/vton/route.ts @@ -72,7 +72,21 @@ async function callVtonApi( const prediction = data.predictions?.[0]; if (!prediction?.bytesBase64Encoded) { - throw new Error("No result from VTON API"); + if (process.env.NODE_ENV === "development") { + console.error( + "[VTON] Empty prediction. Full response:", + JSON.stringify(data, null, 2) + ); + } + const raiReason = prediction?.raiFilteredReason; + const reason = raiReason + ? "RAI_FILTERED" + : prediction?.safetyAttributes?.blocked + ? "RAI_FILTERED" + : data.predictions?.length === 0 + ? "Model returned no predictions — check input images" + : "No result from VTON API"; + throw new Error(reason); } return { diff --git a/packages/web/lib/components/vton/VtonModal.tsx b/packages/web/lib/components/vton/VtonModal.tsx index 05c030b4..b98b9d66 100644 --- a/packages/web/lib/components/vton/VtonModal.tsx +++ b/packages/web/lib/components/vton/VtonModal.tsx @@ -189,7 +189,11 @@ export function VtonModal() { completeBackgroundJob(resultDataUrl, latencyMs), onTryOnError: (message) => { failBackgroundJob(message); - setError(message); + setError( + message === "RAI_FILTERED" + ? "This image combination was filtered by the AI safety system. Try a different photo or item." + : message + ); }, onTryOnFinally: () => { if (stageInterval.current) clearInterval(stageInterval.current); From dd90b2537a2a9d79ee4ba1a5f4edf4a601f7e1bf Mon Sep 17 00:00:00 2001 From: thxforall <113906780+thxforall@users.noreply.github.com> Date: Thu, 21 May 2026 13:35:08 +0900 Subject: [PATCH 3/3] docs: add content studio spec frontmatter --- .../specs/2026-05-14-content-studio-pipeline-v2-design.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/superpowers/specs/2026-05-14-content-studio-pipeline-v2-design.md b/docs/superpowers/specs/2026-05-14-content-studio-pipeline-v2-design.md index d3c7b93d..f92f70ce 100644 --- a/docs/superpowers/specs/2026-05-14-content-studio-pipeline-v2-design.md +++ b/docs/superpowers/specs/2026-05-14-content-studio-pipeline-v2-design.md @@ -1,3 +1,11 @@ +--- +title: Content Studio Pipeline v2 Design +owner: human +status: draft +updated: 2026-05-14 +tags: [ui, api] +--- + # Content Studio Pipeline v2 Design Issue: #530