import { readFile } from 'node:fs/promises'; import { existsSync } from 'node:fs'; import { join, resolve } from 'node:path'; /** * Bridge from the inert Seedance prompt-skill knowledge (Track A) to the live * code path (Track B). * * `references/video/seedance-skills/NN-*.md` are verbatim copies of the upstream * `beshuaxian/higgsfield-seedance2-jineng` skill files. They are rich but * machine-inert: no code parses their prose. This module is the single place * that maps a canonical category id to its reference file, so the id used by * {@link CATEGORY_REGISTRY} stays in lockstep with the on-disk knowledge doc. * * Intentionally structured-only: this resolves the id ⇄ file relationship and * reads the markdown for human/audit use. It does NOT parse prose into prompt * output — the discrete, selectable techniques (hooks, beat templates, style * lines, sound profiles) live in typed registers, keeping output deterministic. */ export interface SeedanceSkillReference { /** Canonical category id — matches the keys of CATEGORY_REGISTRY. */ id: string; /** Human-readable label. */ label: string; /** File name under references/video/seedance-skills/. */ file: string; } const SEEDANCE_SKILLS_SUBDIR = ['references', 'video', 'seedance-skills'] as const; export const SEEDANCE_SKILL_REGISTRY: readonly SeedanceSkillReference[] = [ { id: 'cinematic', label: 'Cinematic / Narrative', file: '01-cinematic.md' }, { id: '3d-cgi', label: '3D / CGI', file: '02-3d-cgi.md' }, { id: 'cartoon', label: 'Cartoon', file: '03-cartoon.md' }, { id: 'comic-to-video', label: 'Comic to Video', file: '04-comic-to-video.md' }, { id: 'fight-scenes', label: 'Fight Scenes', file: '05-fight-scenes.md' }, { id: 'motion-design-ad', label: 'Motion Design Ad', file: '06-motion-design-ad.md' }, { id: 'ecommerce-ad', label: 'E-commerce Ad', file: '07-ecommerce-ad.md' }, { id: 'anime-action', label: 'Anime Action', file: '08-anime-action.md' }, { id: 'product-360', label: 'Product 360 / Turntable', file: '09-product-360.md' }, { id: 'music-video', label: 'Music Video', file: '10-music-video.md' }, { id: 'social-hook', label: 'Social Hook', file: '11-social-hook.md' }, { id: 'brand-story', label: 'Brand Story', file: '12-brand-story.md' }, { id: 'fashion-lookbook', label: 'Fashion Lookbook', file: '13-fashion-lookbook.md' }, { id: 'food-beverage', label: 'Food & Beverage', file: '14-food-beverage.md' }, { id: 'real-estate', label: 'Real Estate', file: '15-real-estate.md' }, ] as const; export const SEEDANCE_SKILL_IDS: string[] = SEEDANCE_SKILL_REGISTRY.map((s) => s.id); function seedanceSkillsDir(root = process.cwd()): string { return join(resolve(root), ...SEEDANCE_SKILLS_SUBDIR); } /** * Resolve a canonical category id to its skill-reference descriptor. * * Throws on an unknown id (no silent fallback), matching `resolveCategory` in * {@link category-registry}. */ export function categoryReferenceDoc(id: string): SeedanceSkillReference { const entry = SEEDANCE_SKILL_REGISTRY.find((s) => s.id === id); if (!entry) { throw new Error(`unknown seedance skill category: ${id}`); } return entry; } /** * Read the on-disk markdown for a category. Returns null for an unknown id or a * missing file (graceful, mirroring `readPromptReference`). */ export async function readCategoryReference( id: string, root = process.cwd(), ): Promise<(SeedanceSkillReference & { content: string }) | null> { const entry = SEEDANCE_SKILL_REGISTRY.find((s) => s.id === id); if (!entry) return null; const path = join(seedanceSkillsDir(root), entry.file); if (!existsSync(path)) return null; const content = await readFile(path, 'utf-8'); return { ...entry, content }; }