diff --git a/ui/src/features/agent/characterKitActions.ts b/ui/src/features/agent/characterKitActions.ts index 76edf3b1..81eae718 100644 --- a/ui/src/features/agent/characterKitActions.ts +++ b/ui/src/features/agent/characterKitActions.ts @@ -1,227 +1 @@ -import { fetchCharacterKitLibrary, fetchOutputs, saveCharacterKit } from '../../api/client' -import { - createCharacterKit, - type CharacterKit, - type CharacterKitLibrary, - type CharacterKitStyle, -} from '../../lib/characterKit' -import { applyFaceRigMouthPreset, FACE_RIG_PRESET_ROOT, type FaceRigMouthPresetPack } from '../../lib/characterKitFaceRig' -import { queueFaceRigHandoff } from '../../lib/characterKitHandoff' -import { useStore } from '../../stores/useStore' -import { rememberCharacterKitLibrary } from './wizardLabSession' -import { executionReport, type AgentExecutionReport } from './agentContract' - -export interface AgentCreateCharacterKitAction { - type: 'create_character_kit' - name: string - style: CharacterKitStyle -} - -export interface AgentOpenCharacterKitAction { - type: 'open_character_kit' - kitName: string -} - -export interface AgentUpdateCharacterKitAction { - type: 'update_character_kit' - kitName: string - name: string - lookNotes: string - style: CharacterKitStyle | '' -} - -export interface AgentAttachCharacterKitReferencesAction { - type: 'attach_character_kit_references' - kitName: string - outputNames: string[] -} - -export interface AgentBuildCharacterKitAction { - type: 'build_character_kit' - kitName: string -} - -export interface AgentOpenCharacterKitRigAction { - type: 'open_character_kit_rig' - kitName: string -} - -export interface AgentApplyCharacterKitPresetAction { - type: 'apply_character_kit_preset' - kitName: string - presetId: string -} - -export interface AgentTrackCharacterKitJobAction { - type: 'track_character_kit_job' - kitName: string -} - -const STYLES = new Set(['cutout', 'children-illustration', 'anime-2d']) - -function workspaceName(): string { - return useStore.getState().activeWorkspace || 'default' -} - -function showCharacterKit(): void { - const state = useStore.getState() - state.setSettingsOpen(false) - state.setDashboardOpen(false) - state.setMediaFilter('characters') - state.setSidebarOpen(false) -} - -function normalizeName(value: string): string { - return value.normalize('NFD').replace(/[\u0300-\u036f]/g, '').trim().toLowerCase() -} - -async function loadLibrary(): Promise { - const library = await fetchCharacterKitLibrary(workspaceName()) - rememberCharacterKitLibrary(library) - return library -} - -function findKit(library: CharacterKitLibrary, kitName: string): CharacterKit { - if (kitName.trim()) { - const wanted = normalizeName(kitName) - const match = Object.values(library.kits).find(kit => normalizeName(kit.name) === wanted || kit.id === kitName) - if (!match) throw new Error(`No existe el Character Kit “${kitName}”.`) - return match - } - const active = library.kits[library.activeId] || Object.values(library.kits)[0] - if (!active) throw new Error('No hay un Character Kit abierto.') - return active -} - -async function persist(library: CharacterKitLibrary, kit: CharacterKit): Promise { - const next = await saveCharacterKit(workspaceName(), library, kit) - rememberCharacterKitLibrary(next) - return next -} - -function kitReport(kit: CharacterKit, message: string, state: AgentExecutionReport['state'] = 'completed'): AgentExecutionReport { - return executionReport({ - state, - message, - recoverable: false, - target: { kind: 'character_kit', id: kit.id, title: kit.name }, - }) -} - -export async function createAgentCharacterKit(action: AgentCreateCharacterKitAction): Promise<{ message: string; report: AgentExecutionReport }> { - const library = await loadLibrary() - const existing = Object.values(library.kits).find(kit => normalizeName(kit.name) === normalizeName(action.name)) - if (existing) { - rememberCharacterKitLibrary({ ...library, activeId: existing.id }) - showCharacterKit() - const message = `He abierto el Character Kit existente “${existing.name}”.` - return { message, report: kitReport(existing, message) } - } - const kit = createCharacterKit(action.name, STYLES.has(action.style) ? action.style : 'cutout') - await persist(library, kit) - showCharacterKit() - const message = `He creado el Character Kit “${kit.name}”. Todavía no he generado poses.` - return { message, report: kitReport(kit, message) } -} - -export async function openAgentCharacterKit(action: AgentOpenCharacterKitAction): Promise<{ message: string; report: AgentExecutionReport }> { - const library = await loadLibrary() - const kit = findKit(library, action.kitName) - rememberCharacterKitLibrary({ ...library, activeId: kit.id }) - showCharacterKit() - const message = `He abierto Character Kit “${kit.name}”.` - return { message, report: kitReport(kit, message) } -} - -export async function updateAgentCharacterKit(action: AgentUpdateCharacterKitAction): Promise<{ message: string; report: AgentExecutionReport }> { - const library = await loadLibrary() - const current = findKit(library, action.kitName) - const kit: CharacterKit = { - ...current, - name: action.name.trim() || current.name, - lookNotes: action.lookNotes.trim() || current.lookNotes, - style: action.style && STYLES.has(action.style) ? action.style : current.style, - updatedAt: new Date().toISOString(), - } - await persist(library, kit) - const message = `He actualizado la identidad de “${kit.name}”.` - return { message, report: kitReport(kit, message) } -} - -export async function attachAgentCharacterKitReferences(action: AgentAttachCharacterKitReferencesAction): Promise<{ message: string; report: AgentExecutionReport }> { - const library = await loadLibrary() - const current = findKit(library, action.kitName) - const outputs = await fetchOutputs(80, 0, { workspace: workspaceName(), mediaType: 'image' }) - const wanted = action.outputNames.map(name => name.trim()).filter(Boolean) - if (wanted.length !== 1) { - throw new Error('Character Kit admite una única referencia de identidad. Indica un solo output exacto.') - } - const identity = outputs.outputs.find(output => output.name === wanted[0]) - if (!identity) throw new Error(`No existe el output de imagen “${wanted[0]}” en este workspace.`) - const kit: CharacterKit = { - ...current, - identityReference: { - id: `${current.id}-identity`, - name: identity.name, - source: identity.url || identity.name, - kind: 'image', - alphaStatus: 'opaque', - reviewState: 'approved', - workspace: workspaceName(), - }, - updatedAt: new Date().toISOString(), - } - await persist(library, kit) - const message = `He adjuntado “${identity.name}” como referencia de identidad de “${kit.name}”.` - return { message, report: kitReport(kit, message) } -} - -export async function buildAgentCharacterKit(action: AgentBuildCharacterKitAction): Promise<{ message: string; report: AgentExecutionReport }> { - const library = await loadLibrary() - const current = findKit(library, action.kitName) - const source = current.identityReference || current.base - if (!source?.source) throw new Error('El kit no tiene una referencia de identidad para construir la pose base.') - const kit: CharacterKit = { - ...current, - base: { ...source, id: `${current.id}-base`, reviewState: 'approved' }, - updatedAt: new Date().toISOString(), - } - await persist(library, kit) - const message = `He montado el kit “${kit.name}” con la pose base. No he lanzado generación.` - return { message, report: kitReport(kit, message) } -} - -export async function openAgentCharacterKitRig(action: AgentOpenCharacterKitRigAction): Promise<{ message: string; report: AgentExecutionReport }> { - const library = await loadLibrary() - const kit = findKit(library, action.kitName) - const source = kit.base?.source || kit.identityReference?.source - if (source) queueFaceRigHandoff({ name: kit.name, source, workspace: workspaceName() }) - showCharacterKit() - const message = `He abierto el Face Rig de “${kit.name}”.` - return { message, report: kitReport(kit, message) } -} - -export async function applyAgentCharacterKitPreset(action: AgentApplyCharacterKitPresetAction): Promise<{ message: string; report: AgentExecutionReport }> { - const library = await loadLibrary() - const current = findKit(library, action.kitName) - const response = await fetch(`${FACE_RIG_PRESET_ROOT}/manifest.json`) - if (!response.ok) throw new Error('No pude cargar los packs de visemas.') - const data = await response.json() as { packs?: FaceRigMouthPresetPack[] } - const pack = (data.packs || []).find(item => item.id === action.presetId || item.label === action.presetId) - if (!pack) throw new Error(`No existe el preset de animación “${action.presetId}”.`) - const kit = applyFaceRigMouthPreset(current, pack, workspaceName()) - await persist(library, kit) - const message = `He aplicado el preset “${pack.label}” al Face Rig de “${kit.name}”.` - return { message, report: kitReport(kit, message) } -} - -export async function trackAgentCharacterKitJob(action: AgentTrackCharacterKitJobAction): Promise<{ message: string; report: AgentExecutionReport }> { - const { inspectCanonicalQueue } = await import('./queueActions') - const library = await loadLibrary() - const kit = findKit(library, action.kitName) - const message = await inspectCanonicalQueue('active') - return { - message: `Sigo el trabajo de “${kit.name}”. ${message}`, - report: kitReport(kit, message, 'running'), - } -} +export * from '../characters/actions' diff --git a/ui/src/features/characters/actions.ts b/ui/src/features/characters/actions.ts new file mode 100644 index 00000000..d45dd112 --- /dev/null +++ b/ui/src/features/characters/actions.ts @@ -0,0 +1,227 @@ +import { fetchCharacterKitLibrary, fetchOutputs, saveCharacterKit } from '../../api/client' +import { + createCharacterKit, + type CharacterKit, + type CharacterKitLibrary, + type CharacterKitStyle, +} from '../../lib/characterKit' +import { applyFaceRigMouthPreset, FACE_RIG_PRESET_ROOT, type FaceRigMouthPresetPack } from '../../lib/characterKitFaceRig' +import { queueFaceRigHandoff } from '../../lib/characterKitHandoff' +import { useStore } from '../../stores/useStore' +import { rememberCharacterKitLibrary } from '../agent/wizardLabSession' +import { executionReport, type AgentExecutionReport } from '../agent/agentContract' + +export interface AgentCreateCharacterKitAction { + type: 'create_character_kit' + name: string + style: CharacterKitStyle +} + +export interface AgentOpenCharacterKitAction { + type: 'open_character_kit' + kitName: string +} + +export interface AgentUpdateCharacterKitAction { + type: 'update_character_kit' + kitName: string + name: string + lookNotes: string + style: CharacterKitStyle | '' +} + +export interface AgentAttachCharacterKitReferencesAction { + type: 'attach_character_kit_references' + kitName: string + outputNames: string[] +} + +export interface AgentBuildCharacterKitAction { + type: 'build_character_kit' + kitName: string +} + +export interface AgentOpenCharacterKitRigAction { + type: 'open_character_kit_rig' + kitName: string +} + +export interface AgentApplyCharacterKitPresetAction { + type: 'apply_character_kit_preset' + kitName: string + presetId: string +} + +export interface AgentTrackCharacterKitJobAction { + type: 'track_character_kit_job' + kitName: string +} + +const STYLES = new Set(['cutout', 'children-illustration', 'anime-2d']) + +function workspaceName(): string { + return useStore.getState().activeWorkspace || 'default' +} + +function showCharacterKit(): void { + const state = useStore.getState() + state.setSettingsOpen(false) + state.setDashboardOpen(false) + state.setMediaFilter('characters') + state.setSidebarOpen(false) +} + +function normalizeName(value: string): string { + return value.normalize('NFD').replace(/[\u0300-\u036f]/g, '').trim().toLowerCase() +} + +async function loadLibrary(): Promise { + const library = await fetchCharacterKitLibrary(workspaceName()) + rememberCharacterKitLibrary(library) + return library +} + +function findKit(library: CharacterKitLibrary, kitName: string): CharacterKit { + if (kitName.trim()) { + const wanted = normalizeName(kitName) + const match = Object.values(library.kits).find(kit => normalizeName(kit.name) === wanted || kit.id === kitName) + if (!match) throw new Error(`No existe el Character Kit “${kitName}”.`) + return match + } + const active = library.kits[library.activeId] || Object.values(library.kits)[0] + if (!active) throw new Error('No hay un Character Kit abierto.') + return active +} + +async function persist(library: CharacterKitLibrary, kit: CharacterKit): Promise { + const next = await saveCharacterKit(workspaceName(), library, kit) + rememberCharacterKitLibrary(next) + return next +} + +function kitReport(kit: CharacterKit, message: string, state: AgentExecutionReport['state'] = 'completed'): AgentExecutionReport { + return executionReport({ + state, + message, + recoverable: false, + target: { kind: 'character_kit', id: kit.id, title: kit.name }, + }) +} + +export async function createAgentCharacterKit(action: AgentCreateCharacterKitAction): Promise<{ message: string; report: AgentExecutionReport }> { + const library = await loadLibrary() + const existing = Object.values(library.kits).find(kit => normalizeName(kit.name) === normalizeName(action.name)) + if (existing) { + rememberCharacterKitLibrary({ ...library, activeId: existing.id }) + showCharacterKit() + const message = `He abierto el Character Kit existente “${existing.name}”.` + return { message, report: kitReport(existing, message) } + } + const kit = createCharacterKit(action.name, STYLES.has(action.style) ? action.style : 'cutout') + await persist(library, kit) + showCharacterKit() + const message = `He creado el Character Kit “${kit.name}”. Todavía no he generado poses.` + return { message, report: kitReport(kit, message) } +} + +export async function openAgentCharacterKit(action: AgentOpenCharacterKitAction): Promise<{ message: string; report: AgentExecutionReport }> { + const library = await loadLibrary() + const kit = findKit(library, action.kitName) + rememberCharacterKitLibrary({ ...library, activeId: kit.id }) + showCharacterKit() + const message = `He abierto Character Kit “${kit.name}”.` + return { message, report: kitReport(kit, message) } +} + +export async function updateAgentCharacterKit(action: AgentUpdateCharacterKitAction): Promise<{ message: string; report: AgentExecutionReport }> { + const library = await loadLibrary() + const current = findKit(library, action.kitName) + const kit: CharacterKit = { + ...current, + name: action.name.trim() || current.name, + lookNotes: action.lookNotes.trim() || current.lookNotes, + style: action.style && STYLES.has(action.style) ? action.style : current.style, + updatedAt: new Date().toISOString(), + } + await persist(library, kit) + const message = `He actualizado la identidad de “${kit.name}”.` + return { message, report: kitReport(kit, message) } +} + +export async function attachAgentCharacterKitReferences(action: AgentAttachCharacterKitReferencesAction): Promise<{ message: string; report: AgentExecutionReport }> { + const library = await loadLibrary() + const current = findKit(library, action.kitName) + const outputs = await fetchOutputs(80, 0, { workspace: workspaceName(), mediaType: 'image' }) + const wanted = action.outputNames.map(name => name.trim()).filter(Boolean) + if (wanted.length !== 1) { + throw new Error('Character Kit admite una única referencia de identidad. Indica un solo output exacto.') + } + const identity = outputs.outputs.find(output => output.name === wanted[0]) + if (!identity) throw new Error(`No existe el output de imagen “${wanted[0]}” en este workspace.`) + const kit: CharacterKit = { + ...current, + identityReference: { + id: `${current.id}-identity`, + name: identity.name, + source: identity.url || identity.name, + kind: 'image', + alphaStatus: 'opaque', + reviewState: 'approved', + workspace: workspaceName(), + }, + updatedAt: new Date().toISOString(), + } + await persist(library, kit) + const message = `He adjuntado “${identity.name}” como referencia de identidad de “${kit.name}”.` + return { message, report: kitReport(kit, message) } +} + +export async function buildAgentCharacterKit(action: AgentBuildCharacterKitAction): Promise<{ message: string; report: AgentExecutionReport }> { + const library = await loadLibrary() + const current = findKit(library, action.kitName) + const source = current.identityReference || current.base + if (!source?.source) throw new Error('El kit no tiene una referencia de identidad para construir la pose base.') + const kit: CharacterKit = { + ...current, + base: { ...source, id: `${current.id}-base`, reviewState: 'approved' }, + updatedAt: new Date().toISOString(), + } + await persist(library, kit) + const message = `He montado el kit “${kit.name}” con la pose base. No he lanzado generación.` + return { message, report: kitReport(kit, message) } +} + +export async function openAgentCharacterKitRig(action: AgentOpenCharacterKitRigAction): Promise<{ message: string; report: AgentExecutionReport }> { + const library = await loadLibrary() + const kit = findKit(library, action.kitName) + const source = kit.base?.source || kit.identityReference?.source + if (source) queueFaceRigHandoff({ name: kit.name, source, workspace: workspaceName() }) + showCharacterKit() + const message = `He abierto el Face Rig de “${kit.name}”.` + return { message, report: kitReport(kit, message) } +} + +export async function applyAgentCharacterKitPreset(action: AgentApplyCharacterKitPresetAction): Promise<{ message: string; report: AgentExecutionReport }> { + const library = await loadLibrary() + const current = findKit(library, action.kitName) + const response = await fetch(`${FACE_RIG_PRESET_ROOT}/manifest.json`) + if (!response.ok) throw new Error('No pude cargar los packs de visemas.') + const data = await response.json() as { packs?: FaceRigMouthPresetPack[] } + const pack = (data.packs || []).find(item => item.id === action.presetId || item.label === action.presetId) + if (!pack) throw new Error(`No existe el preset de animación “${action.presetId}”.`) + const kit = applyFaceRigMouthPreset(current, pack, workspaceName()) + await persist(library, kit) + const message = `He aplicado el preset “${pack.label}” al Face Rig de “${kit.name}”.` + return { message, report: kitReport(kit, message) } +} + +export async function trackAgentCharacterKitJob(action: AgentTrackCharacterKitJobAction): Promise<{ message: string; report: AgentExecutionReport }> { + const { inspectCanonicalQueue } = await import('../agent/queueActions') + const library = await loadLibrary() + const kit = findKit(library, action.kitName) + const message = await inspectCanonicalQueue('active') + return { + message: `Sigo el trabajo de “${kit.name}”. ${message}`, + report: kitReport(kit, message, 'running'), + } +} diff --git a/ui/src/features/characters/adapters.ts b/ui/src/features/characters/adapters.ts new file mode 100644 index 00000000..350b4f19 --- /dev/null +++ b/ui/src/features/characters/adapters.ts @@ -0,0 +1,52 @@ +import { + applyAgentCharacterKitPreset, + attachAgentCharacterKitReferences, + buildAgentCharacterKit, + createAgentCharacterKit, + openAgentCharacterKit, + openAgentCharacterKitRig, + trackAgentCharacterKitJob, + updateAgentCharacterKit, +} from './actions' +import type { + ApplyCharacterKitPresetCommand, + AttachCharacterKitReferencesCommand, + BuildCharacterKitCommand, + CreateCharacterKitCommand, + OpenCharacterKitCommand, + OpenCharacterKitRigCommand, + TrackCharacterKitJobCommand, + UpdateCharacterKitCommand, +} from './commands' + +export async function createKit(command: CreateCharacterKitCommand) { + return createAgentCharacterKit(command) +} + +export async function openKit(command: OpenCharacterKitCommand) { + return openAgentCharacterKit(command) +} + +export async function updateKit(command: UpdateCharacterKitCommand) { + return updateAgentCharacterKit(command) +} + +export async function attachReference(command: AttachCharacterKitReferencesCommand) { + return attachAgentCharacterKitReferences(command) +} + +export async function buildKit(command: BuildCharacterKitCommand) { + return buildAgentCharacterKit(command) +} + +export async function openRig(command: OpenCharacterKitRigCommand) { + return openAgentCharacterKitRig(command) +} + +export async function applyPreset(command: ApplyCharacterKitPresetCommand) { + return applyAgentCharacterKitPreset(command) +} + +export async function trackJob(command: TrackCharacterKitJobCommand) { + return trackAgentCharacterKitJob(command) +} diff --git a/ui/src/features/characters/commands.ts b/ui/src/features/characters/commands.ts new file mode 100644 index 00000000..910116ca --- /dev/null +++ b/ui/src/features/characters/commands.ts @@ -0,0 +1,10 @@ +export type { + AgentApplyCharacterKitPresetAction as ApplyCharacterKitPresetCommand, + AgentAttachCharacterKitReferencesAction as AttachCharacterKitReferencesCommand, + AgentBuildCharacterKitAction as BuildCharacterKitCommand, + AgentCreateCharacterKitAction as CreateCharacterKitCommand, + AgentOpenCharacterKitAction as OpenCharacterKitCommand, + AgentOpenCharacterKitRigAction as OpenCharacterKitRigCommand, + AgentTrackCharacterKitJobAction as TrackCharacterKitJobCommand, + AgentUpdateCharacterKitAction as UpdateCharacterKitCommand, +} from './actions' diff --git a/ui/tests/agentCapabilityPorts.test.mjs b/ui/tests/agentCapabilityPorts.test.mjs index 190b2a6b..0c577584 100644 --- a/ui/tests/agentCapabilityPorts.test.mjs +++ b/ui/tests/agentCapabilityPorts.test.mjs @@ -30,6 +30,9 @@ const SET_STATE_ALLOWLIST = [ ] const SLICE_AGENT_IMPORT_ALLOWLIST = [ + ['characters/actions.ts', '../agent/agentContract'], + ['characters/actions.ts', '../agent/queueActions'], + ['characters/actions.ts', '../agent/wizardLabSession'], ['series/SeriesEpisodePanel.tsx', '../agent/agentUiBus'], ['series/SeriesLabPanel.tsx', '../agent/agentUiBus'], ['series/SeriesReviewPanel.tsx', '../agent/agentUiBus'],