Files
ALwrity/frontend/src/hooks/useStoryWriterState.ts

745 lines
27 KiB
TypeScript

import { useState, useCallback, useEffect } from 'react';
import {
StoryGenerationRequest,
StoryProjectSummary,
CreateStoryProjectRequest,
UpdateStoryProjectRequest,
storyWriterApi,
} from '../services/storyWriterApi';
export interface SceneAnimationResume {
predictionId: string;
duration: 5 | 10;
message?: string;
createdAt?: string;
}
export interface StoryWriterState {
// Persistent project identity
projectId: string | null;
projectTitle: string | null;
// Mode and template selection
storyMode: 'marketing' | 'pure';
storyTemplate: 'product_story' | 'brand_manifesto' | 'founder_story' | 'customer_story' | null;
// Story parameters (Setup phase)
persona: string;
storySetting: string;
characters: string;
plotElements: string;
writingStyle: string;
storyTone: string;
narrativePOV: string;
audienceAgeGroup: string;
contentRating: string;
endingPreference: string;
storyLength: string;
enableExplainer: boolean;
enableIllustration: boolean;
enableNarration: boolean;
enableVideoNarration: boolean;
// Image generation settings
imageProvider: string | null;
imageWidth: number;
imageHeight: number;
imageModel: string | null;
// Video generation settings
videoFps: number;
videoTransitionDuration: number;
// Audio generation settings
audioProvider: string;
audioLang: string;
audioSlow: boolean;
audioRate: number;
// Anime-specific structured data
animeBible: any | null;
// Generated content
premise: string | null;
outline: string | null;
outlineScenes: any[] | null; // Structured scenes from outline
isOutlineStructured: boolean;
storyContent: string | null;
isComplete: boolean;
autoGenerateOnWriting: boolean;
sceneImages: Map<number, string> | null; // Generated image URLs by scene number
sceneAudio: Map<number, string> | null; // Generated audio URLs by scene number
storyVideo: string | null; // Generated video URL
sceneHdVideos: Map<number, string> | null; // Approved HD video URLs by scene number
sceneAnimatedVideos: Map<number, string> | null; // Animated scene preview videos
sceneAnimationResumables: Map<number, SceneAnimationResume> | null; // Pending resume info per scene
hdVideoGenerationStatus: 'idle' | 'generating' | 'awaiting_approval' | 'completed' | 'paused';
currentHdSceneIndex: number; // Which scene is currently being generated/reviewed
// Task management
currentTaskId: string | null;
generationProgress: number;
generationMessage: string | null;
// UI state
isLoading: boolean;
error: string | null;
}
const DEFAULT_STATE: Partial<StoryWriterState> = {
projectId: null,
projectTitle: null,
storyMode: 'marketing',
storyTemplate: 'product_story',
persona: '',
storySetting: '',
characters: '',
plotElements: '',
writingStyle: 'Formal',
storyTone: 'Suspenseful',
narrativePOV: 'Third Person Limited',
audienceAgeGroup: 'Adults (18+)',
contentRating: 'PG-13',
endingPreference: 'Happy',
storyLength: 'Medium',
enableExplainer: true,
enableIllustration: true,
enableNarration: true,
enableVideoNarration: true,
// Image generation settings
imageProvider: null,
imageWidth: 1024,
imageHeight: 1024,
imageModel: null,
// Video generation settings
videoFps: 24,
videoTransitionDuration: 0.5,
// Audio generation settings
audioProvider: 'gtts',
audioLang: 'en',
audioSlow: false,
audioRate: 150,
premise: null,
outline: null,
outlineScenes: null,
isOutlineStructured: false,
storyContent: null,
isComplete: false,
autoGenerateOnWriting: false,
sceneImages: null,
sceneAudio: null,
storyVideo: null,
sceneHdVideos: null,
sceneAnimatedVideos: null,
sceneAnimationResumables: null,
hdVideoGenerationStatus: 'idle',
currentHdSceneIndex: 0,
currentTaskId: null,
generationProgress: 0,
generationMessage: null,
isLoading: false,
error: null,
animeBible: null,
};
// Mapping for old values to new values (for migration)
const AUDIENCE_AGE_GROUP_MIGRATION: Record<string, string> = {
'Adults': 'Adults (18+)',
'Children': 'Children (5-12)',
'Young Adults': 'Young Adults (13-17)',
};
// Valid audience age groups
const VALID_AUDIENCE_AGE_GROUPS = ['Children (5-12)', 'Young Adults (13-17)', 'Adults (18+)', 'All Ages'];
export const useStoryWriterState = () => {
const [state, setState] = useState<StoryWriterState>(() => {
// Initialize from localStorage if available
try {
const saved = localStorage.getItem('story_writer_state');
if (saved) {
const parsed = JSON.parse(saved);
// Migrate old audienceAgeGroup values to new format
if (parsed.audienceAgeGroup && AUDIENCE_AGE_GROUP_MIGRATION[parsed.audienceAgeGroup]) {
parsed.audienceAgeGroup = AUDIENCE_AGE_GROUP_MIGRATION[parsed.audienceAgeGroup];
}
// Validate audienceAgeGroup is in valid list, if not, use default
if (parsed.audienceAgeGroup && !VALID_AUDIENCE_AGE_GROUPS.includes(parsed.audienceAgeGroup)) {
console.warn(`Invalid audienceAgeGroup value: ${parsed.audienceAgeGroup}, using default`);
parsed.audienceAgeGroup = DEFAULT_STATE.audienceAgeGroup;
}
// Convert arrays back to Maps
const restoredState = {
...DEFAULT_STATE,
...parsed,
sceneImages: parsed.sceneImages ? new Map(parsed.sceneImages) : null,
sceneAudio: parsed.sceneAudio ? new Map(parsed.sceneAudio) : null,
sceneHdVideos: parsed.sceneHdVideos ? new Map(parsed.sceneHdVideos) : null,
sceneAnimatedVideos: parsed.sceneAnimatedVideos ? new Map(parsed.sceneAnimatedVideos) : null,
sceneAnimationResumables: parsed.sceneAnimationResumables ? new Map(parsed.sceneAnimationResumables) : null,
};
return restoredState as StoryWriterState;
}
} catch (error) {
console.error('Error loading story writer state from localStorage:', error);
}
return DEFAULT_STATE as StoryWriterState;
});
// Fix invalid audienceAgeGroup values whenever state changes
useEffect(() => {
if (state.audienceAgeGroup && !VALID_AUDIENCE_AGE_GROUPS.includes(state.audienceAgeGroup)) {
// Migrate old values to new format
const migratedValue = AUDIENCE_AGE_GROUP_MIGRATION[state.audienceAgeGroup] || (DEFAULT_STATE.audienceAgeGroup as string);
if (migratedValue !== state.audienceAgeGroup) {
console.log(`Migrating audienceAgeGroup from '${state.audienceAgeGroup}' to '${migratedValue}'`);
setState((prev) => ({ ...prev, audienceAgeGroup: migratedValue }));
}
}
}, [state.audienceAgeGroup]);
// Persist state to localStorage
useEffect(() => {
try {
// Don't persist loading/error states
const { isLoading, error, ...persistableState } = state;
// Ensure audienceAgeGroup is valid before persisting
let validAudienceAgeGroup = persistableState.audienceAgeGroup;
if (!VALID_AUDIENCE_AGE_GROUPS.includes(validAudienceAgeGroup)) {
validAudienceAgeGroup = AUDIENCE_AGE_GROUP_MIGRATION[validAudienceAgeGroup] || (DEFAULT_STATE.audienceAgeGroup as string);
// Update state if corrected
if (validAudienceAgeGroup !== persistableState.audienceAgeGroup) {
setState((prev) => ({ ...prev, audienceAgeGroup: validAudienceAgeGroup }));
}
}
// Convert Maps to arrays for JSON serialization
const serializableState = {
...persistableState,
audienceAgeGroup: validAudienceAgeGroup,
sceneImages: persistableState.sceneImages ? Array.from(persistableState.sceneImages.entries()) : null,
sceneAudio: persistableState.sceneAudio ? Array.from(persistableState.sceneAudio.entries()) : null,
sceneHdVideos: persistableState.sceneHdVideos ? Array.from(persistableState.sceneHdVideos.entries()) : null,
sceneAnimatedVideos: persistableState.sceneAnimatedVideos
? Array.from(persistableState.sceneAnimatedVideos.entries())
: null,
sceneAnimationResumables: persistableState.sceneAnimationResumables
? Array.from(persistableState.sceneAnimationResumables.entries())
: null,
animeBible: persistableState.animeBible || null,
};
localStorage.setItem('story_writer_state', JSON.stringify(serializableState));
} catch (error) {
console.error('Error saving story writer state to localStorage:', error);
}
}, [state]);
// Setters
const setStoryMode = useCallback((storyMode: 'marketing' | 'pure') => {
setState((prev) => ({ ...prev, storyMode }));
}, []);
const setStoryTemplate = useCallback(
(storyTemplate: 'product_story' | 'brand_manifesto' | 'founder_story' | 'customer_story' | null) => {
setState((prev) => ({ ...prev, storyTemplate }));
},
[]);
const setAnimeBible = useCallback((animeBible: any | null) => {
setState((prev) => ({ ...prev, animeBible }));
}, []);
const setPersona = useCallback((persona: string) => {
setState((prev) => ({ ...prev, persona }));
}, []);
const setStorySetting = useCallback((setting: string) => {
setState((prev) => ({ ...prev, storySetting: setting }));
}, []);
const setCharacters = useCallback((characters: string) => {
setState((prev) => ({ ...prev, characters }));
}, []);
const setPlotElements = useCallback((plotElements: string) => {
setState((prev) => ({ ...prev, plotElements }));
}, []);
const setWritingStyle = useCallback((style: string) => {
setState((prev) => ({ ...prev, writingStyle: style }));
}, []);
const setStoryTone = useCallback((tone: string) => {
setState((prev) => ({ ...prev, storyTone: tone }));
}, []);
const setNarrativePOV = useCallback((pov: string) => {
setState((prev) => ({ ...prev, narrativePOV: pov }));
}, []);
const setAudienceAgeGroup = useCallback((ageGroup: string) => {
// Migrate old values to new format
const migratedAgeGroup = AUDIENCE_AGE_GROUP_MIGRATION[ageGroup] || ageGroup;
// Validate the value is in the valid list
if (VALID_AUDIENCE_AGE_GROUPS.includes(migratedAgeGroup)) {
setState((prev) => ({ ...prev, audienceAgeGroup: migratedAgeGroup }));
} else {
console.warn(`Invalid audienceAgeGroup value: ${ageGroup}, using default`);
setState((prev) => ({ ...prev, audienceAgeGroup: DEFAULT_STATE.audienceAgeGroup as string }));
}
}, []);
const setContentRating = useCallback((rating: string) => {
setState((prev) => ({ ...prev, contentRating: rating }));
}, []);
const setEndingPreference = useCallback((ending: string) => {
setState((prev) => ({ ...prev, endingPreference: ending }));
}, []);
const setStoryLength = useCallback((length: string) => {
setState((prev) => ({ ...prev, storyLength: length }));
}, []);
const setEnableExplainer = useCallback((enabled: boolean) => {
setState((prev) => ({ ...prev, enableExplainer: enabled }));
}, []);
const setEnableIllustration = useCallback((enabled: boolean) => {
setState((prev) => ({ ...prev, enableIllustration: enabled }));
}, []);
const setEnableNarration = useCallback((enabled: boolean) => {
setState((prev) => ({ ...prev, enableNarration: enabled }));
}, []);
const setEnableVideoNarration = useCallback((enabled: boolean) => {
setState((prev) => ({ ...prev, enableVideoNarration: enabled }));
}, []);
// Image generation setters
const setImageProvider = useCallback((provider: string | null) => {
setState((prev) => ({ ...prev, imageProvider: provider }));
}, []);
const setImageWidth = useCallback((width: number) => {
setState((prev) => ({ ...prev, imageWidth: width }));
}, []);
const setImageHeight = useCallback((height: number) => {
setState((prev) => ({ ...prev, imageHeight: height }));
}, []);
const setImageModel = useCallback((model: string | null) => {
setState((prev) => ({ ...prev, imageModel: model }));
}, []);
// Video generation setters
const setVideoFps = useCallback((fps: number) => {
setState((prev) => ({ ...prev, videoFps: fps }));
}, []);
const setVideoTransitionDuration = useCallback((duration: number) => {
setState((prev) => ({ ...prev, videoTransitionDuration: duration }));
}, []);
// Audio generation setters
const setAudioProvider = useCallback((provider: string) => {
setState((prev) => ({ ...prev, audioProvider: provider }));
}, []);
const setAudioLang = useCallback((lang: string) => {
setState((prev) => ({ ...prev, audioLang: lang }));
}, []);
const setAudioSlow = useCallback((slow: boolean) => {
setState((prev) => ({ ...prev, audioSlow: slow }));
}, []);
const setAudioRate = useCallback((rate: number) => {
setState((prev) => ({ ...prev, audioRate: rate }));
}, []);
const setPremise = useCallback((premise: string | null) => {
setState((prev) => ({ ...prev, premise }));
}, []);
const setOutline = useCallback((outline: string | null) => {
setState((prev) => ({ ...prev, outline }));
}, []);
const setOutlineScenes = useCallback((scenes: any[] | null) => {
setState((prev) => ({ ...prev, outlineScenes: scenes, isOutlineStructured: scenes !== null && scenes.length > 0 }));
}, []);
const setIsOutlineStructured = useCallback((isStructured: boolean) => {
setState((prev) => ({ ...prev, isOutlineStructured: isStructured }));
}, []);
const setStoryContent = useCallback((content: string | null) => {
setState((prev) => ({ ...prev, storyContent: content }));
}, []);
const setSceneImages = useCallback((images: Map<number, string> | null) => {
setState((prev) => ({ ...prev, sceneImages: images }));
}, []);
const setSceneAnimatedVideos = useCallback((videos: Map<number, string> | null) => {
setState((prev) => ({ ...prev, sceneAnimatedVideos: videos }));
}, []);
const setSceneAnimationResumables = useCallback((resumables: Map<number, SceneAnimationResume> | null) => {
setState((prev) => ({ ...prev, sceneAnimationResumables: resumables }));
}, []);
const setSceneAudio = useCallback((audio: Map<number, string> | null) => {
setState((prev) => ({ ...prev, sceneAudio: audio }));
}, []);
const setStoryVideo = useCallback((video: string | null) => {
setState((prev) => ({ ...prev, storyVideo: video }));
}, []);
const setSceneHdVideos = useCallback((videos: Map<number, string> | null) => {
setState((prev) => ({ ...prev, sceneHdVideos: videos }));
}, []);
const setHdVideoGenerationStatus = useCallback((status: 'idle' | 'generating' | 'awaiting_approval' | 'completed' | 'paused') => {
setState((prev) => ({ ...prev, hdVideoGenerationStatus: status }));
}, []);
const setCurrentHdSceneIndex = useCallback((index: number) => {
setState((prev) => ({ ...prev, currentHdSceneIndex: index }));
}, []);
const setIsComplete = useCallback((complete: boolean) => {
setState((prev) => ({ ...prev, isComplete: complete }));
}, []);
const setAutoGenerateOnWriting = useCallback((autoGenerate: boolean) => {
setState((prev) => ({ ...prev, autoGenerateOnWriting: autoGenerate }));
}, []);
const setCurrentTaskId = useCallback((taskId: string | null) => {
setState((prev) => ({ ...prev, currentTaskId: taskId }));
}, []);
const setGenerationProgress = useCallback((progress: number) => {
setState((prev) => ({ ...prev, generationProgress: progress }));
}, []);
const setGenerationMessage = useCallback((message: string | null) => {
setState((prev) => ({ ...prev, generationMessage: message }));
}, []);
const setIsLoading = useCallback((loading: boolean) => {
setState((prev) => ({ ...prev, isLoading: loading }));
}, []);
const setError = useCallback((error: string | null) => {
setState((prev) => ({ ...prev, error }));
}, []);
const setProjectMeta = useCallback((projectId: string | null, title: string | null) => {
setState((prev) => ({ ...prev, projectId, projectTitle: title }));
}, []);
const mapProjectToState = useCallback((project: StoryProjectSummary): StoryWriterState => {
const outlineScenes = project.outline && Array.isArray((project.outline as any).scenes)
? (project.outline as any).scenes
: null;
return {
...(DEFAULT_STATE as StoryWriterState),
projectId: project.project_id,
projectTitle: project.title || null,
storyMode: (project.story_mode as any) || (DEFAULT_STATE.storyMode as 'marketing' | 'pure'),
storyTemplate: (project.story_template as any) || DEFAULT_STATE.storyTemplate || null,
premise: (project.setup as any)?.premise || null,
persona: (project.setup as any)?.persona || '',
storySetting: (project.setup as any)?.story_setting || '',
characters: (project.setup as any)?.character_input || '',
plotElements: (project.setup as any)?.plot_elements || '',
writingStyle: (project.setup as any)?.writing_style || (DEFAULT_STATE.writingStyle as string),
storyTone: (project.setup as any)?.story_tone || (DEFAULT_STATE.storyTone as string),
narrativePOV: (project.setup as any)?.narrative_pov || (DEFAULT_STATE.narrativePOV as string),
audienceAgeGroup:
(project.setup as any)?.audience_age_group || (DEFAULT_STATE.audienceAgeGroup as string),
contentRating:
(project.setup as any)?.content_rating || (DEFAULT_STATE.contentRating as string),
endingPreference:
(project.setup as any)?.ending_preference || (DEFAULT_STATE.endingPreference as string),
storyLength: (project.setup as any)?.story_length || (DEFAULT_STATE.storyLength as string),
enableExplainer:
(project.setup as any)?.enable_explainer ??
(DEFAULT_STATE.enableExplainer as boolean),
enableIllustration:
(project.setup as any)?.enable_illustration ??
(DEFAULT_STATE.enableIllustration as boolean),
enableNarration:
(project.setup as any)?.enable_narration ??
(DEFAULT_STATE.enableNarration as boolean),
enableVideoNarration:
(project.setup as any)?.enable_video_narration ??
(DEFAULT_STATE.enableVideoNarration as boolean),
outline: (project.outline as any)?.outline_text || null,
outlineScenes,
isOutlineStructured: Boolean(outlineScenes && outlineScenes.length > 0),
storyContent: (project.story_content as any)?.story || null,
isComplete: project.is_complete,
animeBible: project.anime_bible || null,
sceneImages: null,
sceneAudio: null,
storyVideo: (project.media_state as any)?.story_video || null,
sceneHdVideos: null,
sceneAnimatedVideos: null,
sceneAnimationResumables: null,
hdVideoGenerationStatus: (project.media_state as any)?.hd_video_status ||
(DEFAULT_STATE.hdVideoGenerationStatus as any),
currentHdSceneIndex:
(project.media_state as any)?.current_hd_scene_index ||
(DEFAULT_STATE.currentHdSceneIndex as number),
currentTaskId: (project.media_state as any)?.current_task_id || null,
generationProgress:
(project.media_state as any)?.generation_progress ||
(DEFAULT_STATE.generationProgress as number),
generationMessage:
(project.media_state as any)?.generation_message ||
(DEFAULT_STATE.generationMessage as string | null),
isLoading: false,
error: null,
autoGenerateOnWriting: DEFAULT_STATE.autoGenerateOnWriting as boolean,
audioProvider: DEFAULT_STATE.audioProvider as string,
audioLang: DEFAULT_STATE.audioLang as string,
audioSlow: DEFAULT_STATE.audioSlow as boolean,
audioRate: DEFAULT_STATE.audioRate as number,
imageProvider: DEFAULT_STATE.imageProvider as string | null,
imageWidth: DEFAULT_STATE.imageWidth as number,
imageHeight: DEFAULT_STATE.imageHeight as number,
imageModel: DEFAULT_STATE.imageModel as string | null,
videoFps: DEFAULT_STATE.videoFps as number,
videoTransitionDuration: DEFAULT_STATE.videoTransitionDuration as number,
};
}, []);
const loadProjectFromDb = useCallback(async (projectId: string) => {
try {
setIsLoading(true);
setError(null);
const project = await storyWriterApi.loadStoryProject(projectId);
setState(() => mapProjectToState(project));
} catch (error) {
console.error('Error loading story project from database:', error);
throw error;
} finally {
setIsLoading(false);
}
}, [mapProjectToState, setIsLoading, setError]);
const saveProjectToDb = useCallback(async () => {
if (!state.projectId) {
return;
}
try {
const payload: UpdateStoryProjectRequest = {
title: state.projectTitle || undefined,
story_mode: state.storyMode,
story_template: state.storyTemplate,
setup: {
premise: state.premise,
persona: state.persona,
story_setting: state.storySetting,
character_input: state.characters,
plot_elements: state.plotElements,
writing_style: state.writingStyle,
story_tone: state.storyTone,
narrative_pov: state.narrativePOV,
audience_age_group: state.audienceAgeGroup,
content_rating: state.contentRating,
ending_preference: state.endingPreference,
story_length: state.storyLength,
enable_explainer: state.enableExplainer,
enable_illustration: state.enableIllustration,
enable_narration: state.enableNarration,
enable_video_narration: state.enableVideoNarration,
},
outline: state.outline
? {
outline_text: state.outline,
scenes: state.outlineScenes || [],
}
: undefined,
scenes: state.outlineScenes || undefined,
story_content: state.storyContent ? { story: state.storyContent } : undefined,
anime_bible: state.animeBible || undefined,
media_state: state.storyVideo
? {
story_video: state.storyVideo,
hd_video_status: state.hdVideoGenerationStatus,
current_hd_scene_index: state.currentHdSceneIndex,
current_task_id: state.currentTaskId,
generation_progress: state.generationProgress,
generation_message: state.generationMessage,
}
: undefined,
is_complete: state.isComplete,
};
await storyWriterApi.updateStoryProject(state.projectId, payload);
} catch (error) {
console.error('Error saving story project to database:', error);
}
}, [state]);
const initializeProject = useCallback(
async (projectId: string, title: string | null, initialSetup?: CreateStoryProjectRequest) => {
try {
const payload: CreateStoryProjectRequest = {
project_id: projectId,
title: title || undefined,
story_mode: state.storyMode,
story_template: state.storyTemplate,
setup: initialSetup?.setup || {
premise: state.premise,
persona: state.persona,
story_setting: state.storySetting,
character_input: state.characters,
plot_elements: state.plotElements,
writing_style: state.writingStyle,
story_tone: state.storyTone,
narrative_pov: state.narrativePOV,
audience_age_group: state.audienceAgeGroup,
content_rating: state.contentRating,
ending_preference: state.endingPreference,
story_length: state.storyLength,
enable_explainer: state.enableExplainer,
enable_illustration: state.enableIllustration,
enable_narration: state.enableNarration,
enable_video_narration: state.enableVideoNarration,
},
};
await storyWriterApi.createStoryProject(payload);
setProjectMeta(projectId, title);
} catch (error) {
console.error('Error creating story project in database:', error);
setProjectMeta(projectId, title);
}
},
[state, setProjectMeta],
);
// Helper to get request object
const getRequest = useCallback((): StoryGenerationRequest => {
return {
persona: state.persona,
story_setting: state.storySetting,
character_input: state.characters,
plot_elements: state.plotElements,
writing_style: state.writingStyle,
story_tone: state.storyTone,
narrative_pov: state.narrativePOV,
audience_age_group: state.audienceAgeGroup,
content_rating: state.contentRating,
ending_preference: state.endingPreference,
story_length: state.storyLength,
enable_explainer: state.enableExplainer,
enable_illustration: state.enableIllustration,
enable_narration: state.enableNarration,
enable_video_narration: state.enableVideoNarration,
// Image generation settings
image_provider: state.imageProvider || undefined,
image_width: state.imageWidth,
image_height: state.imageHeight,
image_model: state.imageModel || undefined,
// Video generation settings
video_fps: state.videoFps,
video_transition_duration: state.videoTransitionDuration,
// Audio generation settings
audio_provider: state.audioProvider,
audio_lang: state.audioLang,
audio_slow: state.audioSlow,
audio_rate: state.audioRate,
anime_bible: state.animeBible || null,
};
}, [state]);
// Reset state
const resetState = useCallback(() => {
setState(DEFAULT_STATE as StoryWriterState);
try {
if (typeof window !== 'undefined') {
localStorage.removeItem('story_writer_state');
localStorage.removeItem('storywriter_current_phase');
localStorage.removeItem('storywriter_user_selected_phase');
}
} catch (error) {
console.error('Error clearing story studio state from localStorage:', error);
}
}, []);
return {
...state,
setStoryMode,
setStoryTemplate,
setAnimeBible,
setPersona,
setStorySetting,
setCharacters,
setPlotElements,
setWritingStyle,
setStoryTone,
setNarrativePOV,
setAudienceAgeGroup,
setContentRating,
setEndingPreference,
setStoryLength,
setEnableExplainer,
setEnableIllustration,
setEnableNarration,
setEnableVideoNarration,
setImageProvider,
setImageWidth,
setImageHeight,
setImageModel,
setVideoFps,
setVideoTransitionDuration,
setAudioProvider,
setAudioLang,
setAudioSlow,
setAudioRate,
setPremise,
setOutline,
setOutlineScenes,
setIsOutlineStructured,
setStoryContent,
setIsComplete,
setSceneImages,
setSceneAudio,
setStoryVideo,
setSceneHdVideos,
setSceneAnimatedVideos,
setSceneAnimationResumables,
setHdVideoGenerationStatus,
setCurrentHdSceneIndex,
setCurrentTaskId,
setGenerationProgress,
setGenerationMessage,
setIsLoading,
setError,
setAutoGenerateOnWriting,
// Helpers
getRequest,
resetState,
setProjectMeta,
loadProjectFromDb,
saveProjectToDb,
initializeProject,
};
};