feat: add props system and refactor asset library architecture
This commit is contained in:
@@ -21,6 +21,7 @@ const parseMock = vi.hoisted(() => ({
|
||||
chunkContent: vi.fn(() => ['chunk-1', 'chunk-2']),
|
||||
safeParseCharactersResponse: vi.fn(() => ({ new_characters: [] })),
|
||||
safeParseLocationsResponse: vi.fn(() => ({ locations: [] })),
|
||||
safeParsePropsResponse: vi.fn(() => ({ props: [] })),
|
||||
}))
|
||||
|
||||
const persistMock = vi.hoisted(() => ({
|
||||
@@ -30,12 +31,15 @@ const persistMock = vi.hoisted(() => ({
|
||||
newCharacters: 0,
|
||||
updatedCharacters: 0,
|
||||
newLocations: 0,
|
||||
newProps: 0,
|
||||
skippedCharacters: 0,
|
||||
skippedLocations: 0,
|
||||
skippedProps: 0,
|
||||
})),
|
||||
persistAnalyzeGlobalChunk: vi.fn(async (args: { stats: { newCharacters: number; newLocations: number } }) => {
|
||||
persistAnalyzeGlobalChunk: vi.fn(async (args: { stats: { newCharacters: number; newLocations: number; newProps: number } }) => {
|
||||
args.stats.newCharacters += 1
|
||||
args.stats.newLocations += 1
|
||||
args.stats.newProps += 1
|
||||
}),
|
||||
}))
|
||||
|
||||
@@ -63,12 +67,14 @@ vi.mock('@/lib/workers/handlers/analyze-global-parse', () => ({
|
||||
readText: (value: unknown) => (typeof value === 'string' ? value : ''),
|
||||
safeParseCharactersResponse: parseMock.safeParseCharactersResponse,
|
||||
safeParseLocationsResponse: parseMock.safeParseLocationsResponse,
|
||||
safeParsePropsResponse: parseMock.safeParsePropsResponse,
|
||||
}))
|
||||
vi.mock('@/lib/workers/handlers/analyze-global-prompt', () => ({
|
||||
loadAnalyzeGlobalPromptTemplates: vi.fn(() => ({ characterTemplate: 'c', locationTemplate: 'l' })),
|
||||
loadAnalyzeGlobalPromptTemplates: vi.fn(() => ({ characterTemplate: 'c', locationTemplate: 'l', propTemplate: 'p' })),
|
||||
buildAnalyzeGlobalPrompts: vi.fn(() => ({
|
||||
characterPrompt: 'character prompt',
|
||||
locationPrompt: 'location prompt',
|
||||
propPrompt: 'prop prompt',
|
||||
})),
|
||||
}))
|
||||
vi.mock('@/lib/workers/handlers/analyze-global-persist', () => ({
|
||||
@@ -105,7 +111,7 @@ describe('worker analyze-global behavior', () => {
|
||||
analysisModel: 'llm::analysis-1',
|
||||
globalAssetText: '全局设定',
|
||||
characters: [{ id: 'char-1', name: 'Hero', aliases: null, introduction: 'hero intro' }],
|
||||
locations: [{ id: 'loc-1', name: 'Old Town', summary: 'old town summary' }],
|
||||
locations: [{ id: 'loc-1', name: 'Old Town', summary: 'old town summary', assetKind: 'location' }],
|
||||
episodes: [{ id: 'ep-1', name: '第一集', novelText: 'episode text' }],
|
||||
})
|
||||
})
|
||||
@@ -136,10 +142,13 @@ describe('worker analyze-global behavior', () => {
|
||||
newCharacters: 2,
|
||||
updatedCharacters: 0,
|
||||
newLocations: 2,
|
||||
newProps: 2,
|
||||
skippedCharacters: 0,
|
||||
skippedLocations: 0,
|
||||
skippedProps: 0,
|
||||
totalCharacters: 1,
|
||||
totalLocations: 1,
|
||||
totalProps: 0,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
@@ -137,8 +137,10 @@ describe('worker analyze-novel behavior', () => {
|
||||
success: true,
|
||||
characters: [{ id: 'char-new-1' }],
|
||||
locations: [{ id: 'loc-new-1' }],
|
||||
props: [],
|
||||
characterCount: 1,
|
||||
locationCount: 1,
|
||||
propCount: 0,
|
||||
})
|
||||
|
||||
expect(prismaMock.novelPromotionCharacter.create).toHaveBeenCalledWith(
|
||||
|
||||
@@ -137,6 +137,7 @@ describe('worker clips-build behavior', () => {
|
||||
summary: 'first clip',
|
||||
location: 'Old Town',
|
||||
characters: JSON.stringify(['Hero']),
|
||||
props: null,
|
||||
content: 'START one END',
|
||||
},
|
||||
select: { id: true },
|
||||
|
||||
@@ -19,6 +19,9 @@ describe('story-to-script orchestrator retry', () => {
|
||||
if (action === 'analyze_locations') {
|
||||
return { text: JSON.stringify({ locations: [{ name: '地点A' }] }), reasoning: '' }
|
||||
}
|
||||
if (action === 'analyze_props') {
|
||||
return { text: JSON.stringify({ props: [] }), reasoning: '' }
|
||||
}
|
||||
if (action === 'split_clips') {
|
||||
return {
|
||||
text: JSON.stringify([
|
||||
@@ -44,6 +47,7 @@ describe('story-to-script orchestrator retry', () => {
|
||||
promptTemplates: {
|
||||
characterPromptTemplate: '{input} {characters_lib_name} {characters_lib_info}',
|
||||
locationPromptTemplate: '{input} {locations_lib_name}',
|
||||
propPromptTemplate: '{input} {props_lib_name}',
|
||||
clipPromptTemplate: '{input} {locations_lib_name} {characters_lib_name} {characters_introduction}',
|
||||
screenplayPromptTemplate: '{clip_content} {locations_lib_name} {characters_lib_name} {characters_introduction} {clip_id}',
|
||||
},
|
||||
@@ -78,6 +82,7 @@ describe('story-to-script orchestrator retry', () => {
|
||||
promptTemplates: {
|
||||
characterPromptTemplate: '{input} {characters_lib_name} {characters_lib_info}',
|
||||
locationPromptTemplate: '{input} {locations_lib_name}',
|
||||
propPromptTemplate: '{input} {props_lib_name}',
|
||||
clipPromptTemplate: '{input} {locations_lib_name} {characters_lib_name} {characters_introduction}',
|
||||
screenplayPromptTemplate: '{clip_content} {locations_lib_name} {characters_lib_name} {characters_introduction} {clip_id}',
|
||||
},
|
||||
@@ -109,6 +114,7 @@ describe('story-to-script orchestrator retry', () => {
|
||||
promptTemplates: {
|
||||
characterPromptTemplate: '{input} {characters_lib_name} {characters_lib_info}',
|
||||
locationPromptTemplate: '{input} {locations_lib_name}',
|
||||
propPromptTemplate: '{input} {props_lib_name}',
|
||||
clipPromptTemplate: '{input} {locations_lib_name} {characters_lib_name} {characters_introduction}',
|
||||
screenplayPromptTemplate: '{clip_content} {locations_lib_name} {characters_lib_name} {characters_introduction} {clip_id}',
|
||||
},
|
||||
@@ -133,6 +139,12 @@ describe('story-to-script orchestrator retry', () => {
|
||||
reasoning: '',
|
||||
}
|
||||
}
|
||||
if (action === 'analyze_props') {
|
||||
return {
|
||||
text: '{"props":[]}\n{"extra":"ignored"}',
|
||||
reasoning: '',
|
||||
}
|
||||
}
|
||||
if (action === 'split_clips') {
|
||||
return {
|
||||
text: '[{"start":"甲在门口","end":"乙回答","summary":"片段摘要","location":"地点A","characters":["甲"]}]\n{"extra":"ignored"}',
|
||||
@@ -156,6 +168,7 @@ describe('story-to-script orchestrator retry', () => {
|
||||
promptTemplates: {
|
||||
characterPromptTemplate: '{input} {characters_lib_name} {characters_lib_info}',
|
||||
locationPromptTemplate: '{input} {locations_lib_name}',
|
||||
propPromptTemplate: '{input} {props_lib_name}',
|
||||
clipPromptTemplate: '{input} {locations_lib_name} {characters_lib_name} {characters_introduction}',
|
||||
screenplayPromptTemplate: '{clip_content} {locations_lib_name} {characters_lib_name} {characters_introduction} {clip_id}',
|
||||
},
|
||||
@@ -182,6 +195,9 @@ describe('story-to-script orchestrator retry', () => {
|
||||
if (action === 'analyze_locations') {
|
||||
return { text: JSON.stringify({ locations: [{ name: '地点A' }] }), reasoning: '' }
|
||||
}
|
||||
if (action === 'analyze_props') {
|
||||
return { text: JSON.stringify({ props: [] }), reasoning: '' }
|
||||
}
|
||||
if (action === 'split_clips') {
|
||||
return {
|
||||
text: JSON.stringify([
|
||||
@@ -213,6 +229,7 @@ describe('story-to-script orchestrator retry', () => {
|
||||
promptTemplates: {
|
||||
characterPromptTemplate: '{input} {characters_lib_name} {characters_lib_info}',
|
||||
locationPromptTemplate: '{input} {locations_lib_name}',
|
||||
propPromptTemplate: '{input} {props_lib_name}',
|
||||
clipPromptTemplate: '{input} {locations_lib_name} {characters_lib_name} {characters_introduction}',
|
||||
screenplayPromptTemplate: '{clip_content} {locations_lib_name} {characters_lib_name} {characters_introduction} {clip_id}',
|
||||
},
|
||||
@@ -239,6 +256,9 @@ describe('story-to-script orchestrator retry', () => {
|
||||
if (action === 'analyze_locations') {
|
||||
return { text: JSON.stringify({ locations: [{ name: '地点A' }] }), reasoning: '' }
|
||||
}
|
||||
if (action === 'analyze_props') {
|
||||
return { text: JSON.stringify({ props: [] }), reasoning: '' }
|
||||
}
|
||||
if (action === 'split_clips') {
|
||||
return {
|
||||
text: JSON.stringify([
|
||||
@@ -268,6 +288,7 @@ describe('story-to-script orchestrator retry', () => {
|
||||
promptTemplates: {
|
||||
characterPromptTemplate: '{input} {characters_lib_name} {characters_lib_info}',
|
||||
locationPromptTemplate: '{input} {locations_lib_name}',
|
||||
propPromptTemplate: '{input} {props_lib_name}',
|
||||
clipPromptTemplate: '{input} {locations_lib_name} {characters_lib_name} {characters_introduction}',
|
||||
screenplayPromptTemplate: '{clip_content} {locations_lib_name} {characters_lib_name} {characters_introduction} {clip_id}',
|
||||
},
|
||||
|
||||
@@ -29,6 +29,7 @@ const orchestratorMock = vi.hoisted(() => ({
|
||||
const helperMock = vi.hoisted(() => ({
|
||||
persistAnalyzedCharacters: vi.fn(async () => [{ id: 'character-new-1' }]),
|
||||
persistAnalyzedLocations: vi.fn(async () => [{ id: 'location-new-1' }]),
|
||||
persistAnalyzedProps: vi.fn(async () => [{ id: 'prop-new-1' }]),
|
||||
persistClips: vi.fn(async () => [{ clipKey: 'clip-1', id: 'clip-row-1' }]),
|
||||
}))
|
||||
const workflowLeaseMock = vi.hoisted(() => ({
|
||||
@@ -68,8 +69,9 @@ vi.mock('@/lib/prompt-i18n', () => ({
|
||||
PROMPT_IDS: {
|
||||
NP_AGENT_CHARACTER_PROFILE: 'a',
|
||||
NP_SELECT_LOCATION: 'b',
|
||||
NP_AGENT_CLIP: 'c',
|
||||
NP_SCREENPLAY_CONVERSION: 'd',
|
||||
NP_SELECT_PROP: 'c',
|
||||
NP_AGENT_CLIP: 'd',
|
||||
NP_SCREENPLAY_CONVERSION: 'e',
|
||||
},
|
||||
getPromptTemplate: vi.fn(() => 'prompt-template'),
|
||||
}))
|
||||
@@ -79,6 +81,7 @@ vi.mock('@/lib/workers/handlers/story-to-script-helpers', () => ({
|
||||
parseTemperature: vi.fn(() => 0.7),
|
||||
persistAnalyzedCharacters: helperMock.persistAnalyzedCharacters,
|
||||
persistAnalyzedLocations: helperMock.persistAnalyzedLocations,
|
||||
persistAnalyzedProps: helperMock.persistAnalyzedProps,
|
||||
persistClips: helperMock.persistClips,
|
||||
resolveClipRecordId: (clipIdMap: Map<string, string>, clipId: string) => clipIdMap.get(clipId) ?? null,
|
||||
}))
|
||||
@@ -128,7 +131,7 @@ describe('worker story-to-script behavior', () => {
|
||||
id: 'np-project-1',
|
||||
analysisModel: 'llm::analysis-1',
|
||||
characters: [{ id: 'char-1', name: 'Hero', introduction: 'hero intro' }],
|
||||
locations: [{ id: 'loc-1', name: 'Old Town', summary: 'town' }],
|
||||
locations: [{ id: 'loc-1', name: 'Old Town', summary: 'town', assetKind: 'location' }],
|
||||
})
|
||||
|
||||
prismaMock.novelPromotionEpisode.findUnique.mockResolvedValue({
|
||||
@@ -140,7 +143,9 @@ describe('worker story-to-script behavior', () => {
|
||||
orchestratorMock.runStoryToScriptOrchestrator.mockResolvedValue({
|
||||
analyzedCharacters: [{ name: 'New Hero' }],
|
||||
analyzedLocations: [{ name: 'Market' }],
|
||||
clipList: [{ clipId: 'clip-1', content: 'clip content' }],
|
||||
analyzedProps: [{ name: 'Knife', summary: 'bronze dagger' }],
|
||||
propsObject: { props: [{ name: 'Knife', summary: 'bronze dagger' }] },
|
||||
clipList: [{ clipId: 'clip-1', content: 'clip content', props: ['Knife'] }],
|
||||
screenplayResults: [
|
||||
{
|
||||
clipId: 'clip-1',
|
||||
@@ -152,6 +157,7 @@ describe('worker story-to-script behavior', () => {
|
||||
clipCount: 1,
|
||||
screenplaySuccessCount: 1,
|
||||
screenplayFailedCount: 0,
|
||||
propCount: 1,
|
||||
},
|
||||
})
|
||||
})
|
||||
@@ -172,12 +178,13 @@ describe('worker story-to-script behavior', () => {
|
||||
screenplayFailedCount: 0,
|
||||
persistedCharacters: 1,
|
||||
persistedLocations: 1,
|
||||
persistedProps: 1,
|
||||
persistedClips: 1,
|
||||
})
|
||||
|
||||
expect(helperMock.persistClips).toHaveBeenCalledWith({
|
||||
episodeId: 'episode-1',
|
||||
clipList: [{ clipId: 'clip-1', content: 'clip content' }],
|
||||
clipList: [{ clipId: 'clip-1', content: 'clip content', props: ['Knife'] }],
|
||||
})
|
||||
|
||||
expect(prismaMock.novelPromotionClip.update).toHaveBeenCalledWith({
|
||||
@@ -192,6 +199,8 @@ describe('worker story-to-script behavior', () => {
|
||||
orchestratorMock.runStoryToScriptOrchestrator.mockResolvedValueOnce({
|
||||
analyzedCharacters: [],
|
||||
analyzedLocations: [],
|
||||
analyzedProps: [],
|
||||
propsObject: { props: [] },
|
||||
clipList: [],
|
||||
screenplayResults: [
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user