fabricatio-novel 0.1.1__cp313-cp313-manylinux_2_34_x86_64.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of fabricatio-novel might be problematic. Click here for more details.

@@ -0,0 +1 @@
1
+ """An extension of fabricatio."""
@@ -0,0 +1 @@
1
+ """Actions defined in fabricatio-novel."""
@@ -0,0 +1,337 @@
1
+ """This module provides actions related to novel generation and management.
2
+
3
+ It includes classes such as GenerateNovel for creating novels based on prompts,
4
+ and DumpNovel for saving generated novels to a specified file path. These actions
5
+ leverage capabilities from the fabricatio_core and interact with both Python and
6
+ Rust components to perform their tasks.
7
+ """
8
+
9
+ from pathlib import Path
10
+ from typing import Any, ClassVar, List, Optional
11
+
12
+ from fabricatio_character.models.character import CharacterCard
13
+ from fabricatio_core import Action, logger
14
+ from fabricatio_core.utils import ok
15
+
16
+ from fabricatio_novel.capabilities.novel import NovelCompose
17
+ from fabricatio_novel.models.novel import Novel, NovelDraft
18
+ from fabricatio_novel.models.scripting import Script
19
+ from fabricatio_novel.rust import NovelBuilder
20
+
21
+
22
+ class GenerateCharactersFromDraft(NovelCompose, Action):
23
+ """Generate character cards from a NovelDraft."""
24
+
25
+ novel_draft: Optional[NovelDraft] = None
26
+ """
27
+ The novel draft from which to generate characters.
28
+ """
29
+
30
+ output_key: str = "novel_characters"
31
+ """
32
+ Key under which the generated list of CharacterCard will be stored in context.
33
+ """
34
+
35
+ ctx_override: ClassVar[bool] = True
36
+
37
+ async def _execute(self, *_: Any, **cxt) -> List[CharacterCard] | None:
38
+ draft = ok(self.novel_draft, "`novel_draft` is required for character generation")
39
+ logger.info(f"Generating characters for novel draft: '{draft.title}'")
40
+ characters = await self.create_characters(draft)
41
+ if characters is None:
42
+ logger.warn("Character generation returned None.")
43
+ return None
44
+ valid_chars = [c for c in characters if c is not None]
45
+ logger.info(f"Generated {len(valid_chars)} valid character(s).")
46
+ return valid_chars
47
+
48
+
49
+ class GenerateScriptsFromDraftAndCharacters(NovelCompose, Action):
50
+ """Generate chapter scripts from a draft and list of characters."""
51
+
52
+ novel_draft: Optional[NovelDraft] = None
53
+ """
54
+ The novel draft containing chapter synopses.
55
+ """
56
+
57
+ novel_characters: Optional[List[CharacterCard]] = None # ← renamed for clarity & collision avoidance
58
+ """
59
+ List of character cards to be used in script generation.
60
+ """
61
+
62
+ output_key: str = "novel_scripts"
63
+ """
64
+ Key under which the generated list of Script will be stored in context.
65
+ """
66
+
67
+ ctx_override: ClassVar[bool] = True
68
+
69
+ async def _execute(self, *_: Any, **cxt) -> List[Script] | None:
70
+ draft = ok(self.novel_draft)
71
+ characters = ok(self.novel_characters) # ← consume from context as "novel_characters"
72
+ logger.info(f"Generating scripts for '{draft.title}' with {len(characters)} character(s).")
73
+ scripts = await self.create_scripts(draft, characters)
74
+ if scripts is None:
75
+ logger.warn("Script generation returned None.")
76
+ return None
77
+ valid_scripts = [s for s in scripts if s is not None]
78
+ logger.info(f"Generated {len(valid_scripts)} valid script(s).")
79
+ return valid_scripts
80
+
81
+
82
+ class GenerateChaptersFromScripts(NovelCompose, Action):
83
+ """Generate full chapter contents from scripts and characters."""
84
+
85
+ novel_draft: Optional[NovelDraft] = None
86
+ """
87
+ The novel draft (for language, metadata).
88
+ """
89
+
90
+ novel_scripts: Optional[List[Script]] = None # ← renamed
91
+ """
92
+ The list of chapter scripts to expand into full text.
93
+ """
94
+
95
+ novel_characters: Optional[List[CharacterCard]] = None # ← renamed
96
+ """
97
+ The list of characters to provide context.
98
+ """
99
+ chapter_guidance: Optional[str] = None
100
+ """
101
+ Guidance for writing chapter.
102
+ """
103
+
104
+ output_key: str = "novel_chapter_contents"
105
+ """
106
+ Key under which the generated list of chapter content strings will be stored in context.
107
+ """
108
+
109
+ ctx_override: ClassVar[bool] = True
110
+
111
+ async def _execute(self, *_: Any, **cxt) -> List[str] | List[str | None] | None:
112
+ draft = ok(self.novel_draft)
113
+ scripts = ok(self.novel_scripts)
114
+ characters = ok(self.novel_characters)
115
+
116
+ logger.info(f"Generating {len(scripts)} chapter contents for '{draft.title}'.")
117
+ chapter_contents = await self.create_chapters(draft, scripts, characters, self.chapter_guidance)
118
+ if not chapter_contents:
119
+ logger.warn("Chapter content generation returned empty or None.")
120
+ return None
121
+ logger.info(f"Successfully generated {len(chapter_contents)} chapter content(s).")
122
+ return chapter_contents
123
+
124
+
125
+ class AssembleNovelFromComponents(NovelCompose, Action):
126
+ """Assemble final Novel object from draft, scripts, and chapter contents."""
127
+
128
+ novel_draft: Optional[NovelDraft] = None
129
+ """
130
+ The original draft containing title, synopsis, etc.
131
+ """
132
+
133
+ novel_scripts: Optional[List[Script]] = None # ← renamed
134
+ """
135
+ Scripts containing chapter titles and metadata.
136
+ """
137
+
138
+ novel_chapter_contents: Optional[List[str]] = None # ← renamed
139
+ """
140
+ Generated full text for each chapter.
141
+ """
142
+
143
+ output_key: str = "novel"
144
+ """
145
+ Key under which the assembled Novel object will be stored in context.
146
+ """
147
+
148
+ ctx_override: ClassVar[bool] = True
149
+
150
+ async def _execute(self, *_: Any, **cxt) -> Novel:
151
+ draft = ok(self.novel_draft)
152
+ scripts = ok(self.novel_scripts)
153
+ chapter_contents = ok(self.novel_chapter_contents)
154
+
155
+ logger.info("Assembling final novel from components...")
156
+ novel = self.assemble_novel(draft, scripts, chapter_contents)
157
+ logger.info(f"Novel '{novel.title}' assembled with {len(novel.chapters)} chapters.")
158
+ return novel
159
+
160
+
161
+ class ValidateNovel(Action):
162
+ """Validate the generated novel for compliance and structure."""
163
+
164
+ novel: Optional[Novel] = None
165
+ """
166
+ The novel to validate.
167
+ """
168
+
169
+ output_key: str = "novel_is_valid"
170
+ """
171
+ Key under which the validation result (bool) will be stored in context.
172
+ """
173
+
174
+ min_chapters: int = 1
175
+ min_total_words: int = 1000
176
+ min_compliance_ratio: float = 0.8
177
+
178
+ ctx_override: ClassVar[bool] = True
179
+
180
+ async def _execute(self, *_: Any, **cxt) -> bool:
181
+ novel = ok(self.novel)
182
+
183
+ issues = []
184
+
185
+ if len(novel.chapters) < self.min_chapters:
186
+ issues.append(f"Too few chapters: {len(novel.chapters)} < {self.min_chapters}")
187
+
188
+ if novel.exact_word_count < self.min_total_words:
189
+ issues.append(f"Too few words: {novel.exact_word_count} < {self.min_total_words}")
190
+
191
+ if novel.word_count_compliance_ratio < self.min_compliance_ratio:
192
+ issues.append(
193
+ f"Low compliance ratio: {novel.word_count_compliance_ratio:.2%} < {self.min_compliance_ratio:.2%}"
194
+ )
195
+
196
+ if issues:
197
+ logger.warn(f"Novel validation failed for '{novel.title}': {'; '.join(issues)}")
198
+ return False
199
+ logger.info(f"Novel '{novel.title}' passed validation.")
200
+ return True
201
+
202
+
203
+ class GenerateNovelDraft(NovelCompose, Action):
204
+ """Generate a novel draft from a prompt."""
205
+
206
+ novel_outline: Optional[str] = None
207
+ """
208
+ The prompt used to generate the novel. If not provided, execution will fail.
209
+ """
210
+
211
+ novel_language: Optional[str] = None
212
+ """
213
+ The language of the novel. If not provided, will infer from the prompt.
214
+ """
215
+
216
+ output_key: str = "novel_draft"
217
+ """
218
+ Key under which the generated NovelDraft will be stored in context.
219
+ """
220
+
221
+ ctx_override: ClassVar[bool] = True
222
+
223
+ async def _execute(self, *_: Any, **cxt) -> NovelDraft | None:
224
+ return await self.create_draft(outline=ok(self.novel_outline), language=self.novel_language)
225
+
226
+
227
+ class GenerateNovel(NovelCompose, Action):
228
+ """An action that generates a novel based on a provided prompt.
229
+
230
+ This class inherits from NovelCompose and Action, and is responsible for
231
+ generating a novel using the underlying novel generation capability.
232
+ The generated novel is returned as a Novel object.
233
+ """
234
+
235
+ novel_outline: Optional[str] = None
236
+ """
237
+ The prompt used to generate the novel. If not provided, execution will fail.
238
+ """
239
+
240
+ novel_language: Optional[str] = None
241
+ """
242
+ The language of the novel. If not provided, will infer from the prompt.
243
+ """
244
+
245
+ chapter_guidance: Optional[str] = None
246
+ """
247
+ Guidance for writing chapter.
248
+ """
249
+
250
+ output_key: str = "novel"
251
+ """
252
+ The key under which the generated novel will be stored in the context.
253
+ """
254
+
255
+ ctx_override: ClassVar[bool] = True
256
+
257
+ async def _execute(self, **cxt) -> Novel | None:
258
+ """Execute the novel generation process.
259
+
260
+ Uses the provided novel_prompt to generate a novel via the inherited
261
+ novel() method from NovelCompose. Returns the generated Novel object.
262
+
263
+ Parameters:
264
+ **cxt: Contextual keyword arguments passed from the execution environment.
265
+
266
+ Returns:
267
+ Novel | None: The generated novel object, or None if generation fails.
268
+ """
269
+ return await self.compose_novel(ok(self.novel_outline), self.novel_language, self.chapter_guidance)
270
+
271
+
272
+ class DumpNovel(Action):
273
+ """An action that saves a generated novel to a specified file path.
274
+
275
+ This class takes a Novel object and writes its content to a file at the
276
+ specified path.
277
+ """
278
+
279
+ output_path: Optional[Path] = None
280
+ """
281
+ The file system path where the novel should be saved. Required for execution.
282
+ """
283
+
284
+ novel_font_file: Optional[Path] = None
285
+ """
286
+ The file system path to the novel font file. like .ttf file.
287
+ """
288
+
289
+ novel: Optional[Novel] = None
290
+ """
291
+ The novel object to be saved. Must be provided for successful execution.
292
+ """
293
+
294
+ cover_image: Optional[Path] = None
295
+ """
296
+ The file system path to the novel cover image.
297
+ """
298
+
299
+ output_key: str = "novel_path"
300
+ """
301
+ The key under which the output path will be stored in the context.
302
+ """
303
+
304
+ ctx_override: ClassVar[bool] = True
305
+
306
+ async def _execute(self, *_: Any, **cxt) -> Path:
307
+ novel = ok(self.novel)
308
+ path = ok(self.output_path)
309
+ logger.info(
310
+ f"Novel word count: [{novel.exact_word_count}/{novel.expected_word_count}] | Compliance ratio: {novel.word_count_compliance_ratio:.2%}"
311
+ )
312
+ logger.info(f"Novel Chapter count: {len(novel.chapters)}")
313
+ logger.info(f"Dumping novel {novel.title} to {path}")
314
+
315
+ builder = (
316
+ NovelBuilder()
317
+ .new_novel()
318
+ .set_title(novel.title)
319
+ .set_description(novel.synopsis)
320
+ .add_css("p { text-indent: 2em; margin: 1em 0; line-height: 1.5; text-align: justify; }")
321
+ )
322
+
323
+ if self.novel_font_file:
324
+ (
325
+ builder.add_font(self.novel_font_file.stem, self.novel_font_file).add_css(
326
+ f"p {{ font-family: '{self.novel_font_file.stem}', 'sans-serif'; }}"
327
+ )
328
+ )
329
+
330
+ if self.cover_image:
331
+ builder.add_cover_image(self.cover_image.name, self.cover_image)
332
+
333
+ for chapter in novel.chapters:
334
+ builder.add_chapter(chapter.title, chapter.to_xhtml())
335
+
336
+ builder.export(path)
337
+ return path
@@ -0,0 +1 @@
1
+ """Capabilities defined in fabricatio-novel."""
@@ -0,0 +1,225 @@
1
+ """This module contains the capabilities for the novel."""
2
+
3
+ from abc import ABC
4
+ from typing import List, Optional, Unpack
5
+
6
+ from fabricatio_character.capabilities.character import CharacterCompose
7
+ from fabricatio_character.models.character import CharacterCard
8
+ from fabricatio_character.utils import dump_card
9
+ from fabricatio_core import TEMPLATE_MANAGER, logger
10
+ from fabricatio_core.capabilities.propose import Propose
11
+ from fabricatio_core.capabilities.usages import UseLLM
12
+ from fabricatio_core.models.kwargs_types import ValidateKwargs
13
+ from fabricatio_core.rust import detect_language
14
+ from fabricatio_core.utils import ok, override_kwargs
15
+
16
+ from fabricatio_novel.config import novel_config
17
+ from fabricatio_novel.models.novel import Chapter, Novel, NovelDraft
18
+ from fabricatio_novel.models.scripting import Script
19
+ from fabricatio_novel.rust import text_to_xhtml_paragraphs
20
+
21
+
22
+ class NovelCompose(CharacterCompose, Propose, UseLLM, ABC):
23
+ """This class contains the capabilities for the novel."""
24
+
25
+ async def compose_novel(
26
+ self,
27
+ outline: str,
28
+ language: Optional[str] = None,
29
+ chapter_guidance: Optional[str] = None,
30
+ **kwargs: Unpack[ValidateKwargs[Novel]],
31
+ ) -> Novel | None:
32
+ """Main novel composition pipeline."""
33
+ logger.info(f"Starting novel generation for outline: {outline[:100]}...")
34
+ okwargs = override_kwargs(kwargs, default=None)
35
+
36
+ # Step 1: Generate draft
37
+ logger.debug("Step 1: Generating novel draft from outline")
38
+ draft = ok(await self.create_draft(outline, language, **okwargs))
39
+ if not draft:
40
+ logger.warn("Failed to generate novel draft.")
41
+ return None
42
+ logger.info(f"Draft generated successfully: '{draft.title}' in {draft.language}")
43
+
44
+ # Step 2: Generate characters
45
+ logger.debug("Step 2: Generating character cards from draft")
46
+ characters: List[CharacterCard] = [
47
+ c for c in ok(await self.create_characters(draft, **okwargs)) if c is not None
48
+ ]
49
+ logger.info(f"Generated {len(characters)} valid character(s)")
50
+
51
+ # Step 3: Generate scripts
52
+ logger.debug("Step 3: Generating chapter scripts using draft and characters")
53
+ scripts = ok(await self.create_scripts(draft, characters, **okwargs))
54
+ clean_scripts = [s for s in scripts if s is not None]
55
+ if not clean_scripts:
56
+ logger.warn("No valid scripts were generated from the draft and characters.")
57
+ return None
58
+ logger.info(f"Successfully generated {len(clean_scripts)} script(s) for chapters")
59
+
60
+ # Step 4: Generate chapter contents
61
+ logger.debug("Step 4: Generating full chapter contents from scripts")
62
+ chapter_contents = await self.create_chapters(draft, clean_scripts, characters, chapter_guidance, **okwargs)
63
+ if not chapter_contents:
64
+ logger.warn("Chapter content generation returned no results.")
65
+ return None
66
+ logger.info(f"Generated {len(chapter_contents)} chapter content(s)")
67
+
68
+ # Step 5: Assemble final novel
69
+ logger.debug("Step 5: Assembling final novel from components")
70
+ novel = self.assemble_novel(draft, clean_scripts, chapter_contents)
71
+ logger.info(f"Novel assembly complete: '{novel.title}', {len(novel.chapters)} chapters")
72
+ return novel
73
+
74
+ async def create_draft(
75
+ self, outline: str, language: Optional[str] = None, **kwargs: Unpack[ValidateKwargs[NovelDraft]]
76
+ ) -> NovelDraft | None:
77
+ """Generate a draft for the novel based on the provided outline."""
78
+ logger.debug(f"Creating draft with outline: {outline[:200]}...")
79
+ detected_language = language or detect_language(outline)
80
+ logger.debug(f"Detected language: {detected_language}")
81
+
82
+ prompt = TEMPLATE_MANAGER.render_template(
83
+ novel_config.novel_draft_requirement_template,
84
+ {"outline": outline, "language": detected_language},
85
+ )
86
+ logger.debug(f"Rendered draft prompt:\n{prompt}")
87
+
88
+ result = await self.propose(NovelDraft, prompt, **kwargs)
89
+ if result:
90
+ logger.info(f"Draft created successfully: '{result.title}' ({result.expected_word_count} words)")
91
+ else:
92
+ logger.warn("Draft generation returned None.")
93
+ return result
94
+
95
+ async def create_characters(
96
+ self, draft: NovelDraft, **kwargs: Unpack[ValidateKwargs[CharacterCard]]
97
+ ) -> None | List[CharacterCard] | List[CharacterCard | None]:
98
+ """Generate characters based on draft."""
99
+ logger.debug(f"Generating characters for novel: '{draft.title}'")
100
+ if not draft.character_descriptions:
101
+ logger.warn("No character descriptions found in draft.")
102
+ return []
103
+
104
+ character_prompts = [
105
+ {
106
+ "novel_title": draft.title,
107
+ "synopsis": draft.synopsis,
108
+ "character_desc": c,
109
+ "language": draft.language,
110
+ }
111
+ for c in draft.character_descriptions
112
+ ]
113
+ logger.debug(f"Prepared {len(character_prompts)} character prompts")
114
+
115
+ character_requirement = TEMPLATE_MANAGER.render_template(
116
+ novel_config.character_requirement_template, character_prompts
117
+ )
118
+ logger.debug(f"Character requirement template rendered (length: {len(character_requirement)})")
119
+
120
+ result = await self.compose_characters(character_requirement, **kwargs)
121
+ valid_chars = [c for c in (ok(result) or []) if c is not None]
122
+ logger.info(f"Generated {len(valid_chars)} valid character(s) out of {len(result or [])}")
123
+ return result
124
+
125
+ async def create_scripts(
126
+ self, draft: NovelDraft, characters: List[CharacterCard], **kwargs: Unpack[ValidateKwargs[Script]]
127
+ ) -> List[Script] | List[Script | None] | None:
128
+ """Generate chapter scripts based on draft and characters."""
129
+ logger.debug(f"Generating {len(draft.chapter_synopses)} chapter scripts for '{draft.title}'")
130
+ if not characters:
131
+ logger.warn("No characters provided for script generation.")
132
+ return []
133
+ if not draft.chapter_synopses:
134
+ logger.warn("No chapter synopses in draft.")
135
+ return []
136
+
137
+ character_prompt = dump_card(*characters)
138
+ logger.debug(f"Serialized {len(characters)} character(s) into prompt format")
139
+
140
+ script_prompts = [
141
+ {
142
+ "novel_title": draft.title,
143
+ "characters": character_prompt,
144
+ "synopsis": s,
145
+ "language": draft.language,
146
+ "expected_word_count": c,
147
+ }
148
+ for (s, c) in zip(draft.chapter_synopses, draft.chapter_expected_word_counts, strict=False)
149
+ ]
150
+ logger.debug(f"Created {len(script_prompts)} script input prompts")
151
+
152
+ script_requirement = TEMPLATE_MANAGER.render_template(novel_config.script_requirement_template, script_prompts)
153
+ logger.debug(f"Script requirement template rendered (length: {len(script_requirement)})")
154
+
155
+ result = await self.propose(Script, script_requirement, **kwargs)
156
+ if result is None:
157
+ logger.warn("Script proposal returned None.")
158
+ else:
159
+ valid_scripts = [s for s in result if s is not None]
160
+ logger.info(f"Generated {len(valid_scripts)} valid script(s) out of {len(result)}")
161
+ return result
162
+
163
+ async def create_chapters(
164
+ self,
165
+ draft: NovelDraft,
166
+ scripts: List[Script],
167
+ characters: List[CharacterCard],
168
+ guidance: Optional[str] = None,
169
+ **kwargs: Unpack[ValidateKwargs[str]],
170
+ ) -> List[str] | List[str | None]:
171
+ """Generate actual chapter contents from scripts."""
172
+ logger.debug(f"Generating chapter contents for {len(scripts)} script(s)")
173
+ if not scripts:
174
+ logger.warn("No scripts provided for chapter generation.")
175
+ return []
176
+
177
+ character_prompt = dump_card(*characters)
178
+ logger.debug(f"Using {len(characters)} character(s) context for chapter generation")
179
+
180
+ chapter_prompts = [
181
+ {
182
+ "script": s.as_prompt(),
183
+ "characters": character_prompt,
184
+ "language": draft.language,
185
+ "guidance": guidance,
186
+ "expected_word_count": s.expected_word_count,
187
+ }
188
+ for s in scripts
189
+ ]
190
+ logger.debug(f"Prepared {len(chapter_prompts)} chapter generation prompts")
191
+
192
+ chapter_requirement: List[str] = TEMPLATE_MANAGER.render_template(
193
+ novel_config.chapter_requirement_template, chapter_prompts
194
+ )
195
+ logger.debug(f"Chapter requirement template length: {len(chapter_requirement)}")
196
+
197
+ response = ok(await self.aask(chapter_requirement, **kwargs))
198
+
199
+ logger.info(f"Generated {len(response)} chapter content(s)")
200
+ return response
201
+
202
+ @staticmethod
203
+ def assemble_novel(draft: NovelDraft, scripts: List[Script], chapter_contents: List[str]) -> Novel:
204
+ """Assemble the final novel from components."""
205
+ logger.debug("Assembling final novel from draft, scripts, and chapter contents")
206
+ if len(chapter_contents) != len(scripts):
207
+ logger.warn(
208
+ f"Mismatch between number of scripts ({len(scripts)}) and chapter contents ({len(chapter_contents)})"
209
+ )
210
+
211
+ chapters = []
212
+ for i, (content, script) in enumerate(zip(chapter_contents, scripts, strict=False)):
213
+ title = script.title or f"Chapter {i + 1}"
214
+ cleaned_content = text_to_xhtml_paragraphs(content)
215
+ chapters.append(Chapter(title=title, content=cleaned_content, expected_word_count=0))
216
+ logger.info(f"Assembled {len(chapters)} chapter(s) into the final novel structure")
217
+
218
+ novel = Novel(
219
+ title=draft.title,
220
+ chapters=chapters,
221
+ synopsis=draft.synopsis,
222
+ expected_word_count=draft.expected_word_count,
223
+ )
224
+ logger.debug(f"Final novel assembled: '{novel.title}', total chapters: {len(novel.chapters)}")
225
+ return novel
@@ -0,0 +1,148 @@
1
+ """Fabricatio Novel CLI.
2
+
3
+ This module provides a command-line interface to generate novels using AI-driven workflows.
4
+ It utilizes the Fabricatio Core library and includes functionality for generating novels
5
+ with customizable outlines, chapter guidance, language options, styling, and more.
6
+ """
7
+
8
+ from pathlib import Path
9
+ from typing import NoReturn
10
+
11
+ from fabricatio_core.utils import cfg
12
+
13
+ cfg("fabricatio_novel.workflows", "questionary", "typer", feats=["cli"])
14
+ import typer
15
+ from fabricatio_core import Event, Role, Task
16
+
17
+ from fabricatio_novel.workflows.novel import DebugNovelWorkflow
18
+
19
+ app = typer.Typer(help="A CLI tool to generate novels using AI-driven workflows.")
20
+
21
+ # Register the writer role and workflow
22
+ writer_role = (
23
+ Role(name="writer").register_workflow(Event.quick_instantiate(ns := "write"), DebugNovelWorkflow).dispatch()
24
+ )
25
+
26
+
27
+ def _exit_on_error(message: str) -> NoReturn:
28
+ """Helper to display error and exit."""
29
+ typer.secho(message, fg=typer.colors.RED, bold=True)
30
+ raise typer.Exit(code=1) from None
31
+
32
+
33
+ @app.command(name="w")
34
+ def write_novel( # noqa: PLR0913
35
+ outline: str = typer.Option(
36
+ None, "--outline", "-o", help="The novel's outline or premise.", envvar="NOVEL_OUTLINE"
37
+ ),
38
+ outline_file: Path = typer.Option(
39
+ None,
40
+ "--outline-file",
41
+ "-of",
42
+ exists=True,
43
+ file_okay=True,
44
+ dir_okay=False,
45
+ resolve_path=True,
46
+ help="Path to a text file containing the novel outline.",
47
+ envvar="NOVEL_OUTLINE_FILE",
48
+ ),
49
+ output_path: Path = typer.Option(
50
+ "./novel.epub", "--output", "-out", dir_okay=False, help="Output EPUB file path.", envvar="NOVEL_OUTPUT_PATH"
51
+ ),
52
+ font_file: Path = typer.Option(
53
+ None,
54
+ "--font",
55
+ "-f",
56
+ exists=True,
57
+ dir_okay=False,
58
+ help="Path to custom font file (TTF).",
59
+ envvar="NOVEL_FONT_FILE",
60
+ ),
61
+ cover_image: Path = typer.Option(
62
+ None,
63
+ "--cover",
64
+ "-c",
65
+ exists=True,
66
+ dir_okay=False,
67
+ help="Path to cover image (PNG/JPG/WEBP).",
68
+ envvar="NOVEL_COVER_IMAGE",
69
+ ),
70
+ language: str = typer.Option(
71
+ "English", "--lang", "-l", help="Language of the novel (e.g., 简体中文, English, jp).", envvar="NOVEL_LANGUAGE"
72
+ ),
73
+ chapter_guidance: str = typer.Option(
74
+ None, "--guidance", "-g", help="Guidelines for chapter generation.", envvar="NOVEL_CHAPTER_GUIDANCE"
75
+ ),
76
+ guidance_file: Path = typer.Option(
77
+ None,
78
+ "--guidance-file",
79
+ "-gf",
80
+ exists=True,
81
+ file_okay=True,
82
+ dir_okay=False,
83
+ resolve_path=True,
84
+ help="Path to a text file containing chapter generation guidelines.",
85
+ envvar="NOVEL_GUIDANCE_FILE",
86
+ ),
87
+ persist_dir: Path = typer.Option(
88
+ "./persist", "--persist-dir", help="Directory to save intermediate states.", envvar="NOVEL_PERSIST_DIR"
89
+ ),
90
+ ) -> None:
91
+ """Generate a novel based on the provided outline and settings."""
92
+ # Check mutual exclusivity for outline
93
+ if outline is not None and outline_file is not None:
94
+ _exit_on_error("❌ Cannot use both --outline and --outline-file. Please use only one.")
95
+
96
+ if outline is None and outline_file is None:
97
+ _exit_on_error("❌ Either --outline or --outline-file must be provided.")
98
+
99
+ # Read outline
100
+ try:
101
+ outline_content = outline_file.read_text(encoding="utf-8").strip() if outline_file else outline.strip()
102
+ except (OSError, IOError) as e:
103
+ _exit_on_error(f"❌ Failed to read outline file: {e}")
104
+
105
+ # Check mutual exclusivity for guidance
106
+ if chapter_guidance is not None and guidance_file is not None:
107
+ _exit_on_error("❌ Cannot use both --guidance and --guidance-file. Please use only one.")
108
+
109
+ # Read guidance
110
+ try:
111
+ if guidance_file:
112
+ guidance_content = guidance_file.read_text(encoding="utf-8").strip()
113
+ elif chapter_guidance is not None:
114
+ guidance_content = chapter_guidance.strip()
115
+ else:
116
+ guidance_content = ""
117
+ except (OSError, IOError) as e:
118
+ _exit_on_error(f"❌ Failed to read guidance file: {e}")
119
+
120
+ typer.echo(f"Starting novel generation: '{outline_content[:30]}...'")
121
+
122
+ task = Task(name="Write novel").update_init_context(
123
+ novel_outline=outline_content,
124
+ output_path=output_path,
125
+ novel_font_file=font_file,
126
+ cover_image=cover_image,
127
+ novel_language=language,
128
+ chapter_guidance=guidance_content,
129
+ persist_dir=persist_dir,
130
+ )
131
+
132
+ result = task.delegate_blocking(ns)
133
+
134
+ if result:
135
+ typer.secho(f"✅ Novel successfully generated: {result}", fg=typer.colors.GREEN, bold=True)
136
+ else:
137
+ _exit_on_error("❌ Failed to generate novel.")
138
+
139
+
140
+ @app.command()
141
+ def info() -> None:
142
+ """Show information about this CLI tool."""
143
+ typer.echo("📘 Fabricatio Novel Generator CLI")
144
+ typer.echo("Generate AI-assisted novels in various languages with customizable styling.")
145
+ typer.echo("Powered by Fabricatio Core & DebugNovelWorkflow.")
146
+
147
+
148
+ __all__ = ["app"]
@@ -0,0 +1,30 @@
1
+ """Module containing configuration classes for fabricatio-novel."""
2
+
3
+ from dataclasses import dataclass
4
+
5
+ from fabricatio_core import CONFIG
6
+
7
+
8
+ @dataclass(frozen=True)
9
+ class NovelConfig:
10
+ """Configuration for fabricatio-novel."""
11
+
12
+ render_script_template: str = "render_script"
13
+ """template used to render scripts."""
14
+
15
+ character_requirement_template: str = "character_requirement"
16
+ """template used to render character requirements."""
17
+ script_requirement_template: str = "script_requirement"
18
+ """template used to render script requirements."""
19
+ chapter_requirement_template: str = "chapter_requirement"
20
+ """template used to render chapter requirements."""
21
+ render_chapter_xhtml_template: str = "render_chapter_xhtml"
22
+ """template used to render chapter xhtml."""
23
+ novel_draft_requirement_template: str = "novel_draft_requirement"
24
+ """template used to render novel draft requirements."""
25
+
26
+
27
+ novel_config = CONFIG.load("novel", NovelConfig)
28
+
29
+
30
+ __all__ = ["novel_config"]
@@ -0,0 +1 @@
1
+ """Models defined in fabricatio-novel."""
@@ -0,0 +1,96 @@
1
+ """This module contains the models for the novel."""
2
+
3
+ from typing import Any, List
4
+
5
+ from fabricatio_capabilities.models.generic import PersistentAble, WordCount
6
+ from fabricatio_core import TEMPLATE_MANAGER
7
+ from fabricatio_core.models.generic import Language, SketchedAble, Titled
8
+ from fabricatio_core.rust import logger, word_count
9
+
10
+ from fabricatio_novel.config import novel_config
11
+
12
+
13
+ class NovelDraft(SketchedAble, Titled, Language, PersistentAble, WordCount):
14
+ """A draft representing a novel, including its title, genre, characters, chapters, and synopsis."""
15
+
16
+ title: str
17
+ """The title of the novel."""
18
+ genre: List[str]
19
+ """The genres of the novel. Comprehensive coverage is preferred than few ones."""
20
+
21
+ synopsis: str
22
+ """A summary of the novel's plot."""
23
+
24
+ character_descriptions: List[str]
25
+ """
26
+ Every string in this list MUST be at least 180 words.
27
+ Super detailed descriptions for each main character.
28
+ Include: looks, personality, backstory, goals, relationships, inner struggles, and their role in the story.
29
+ Goal: Make every character feel real, consistent, and fully fleshed out — no vague or shallow summaries.
30
+ """
31
+
32
+ chapter_synopses: List[str]
33
+ """
34
+ Every string in this list MUST be at least 270 words.
35
+ Super detailed summaries for each chapter.
36
+ Cover: what happens, how characters change, key scenes/dialogue, setting shifts, emotional tone, and hints or themes.
37
+ Goal: Lock in every important detail so nothing gets lost later — like a mini-script for each chapter.
38
+ """
39
+ expected_word_count: int
40
+ """The expected word count of the novel."""
41
+
42
+ chapter_expected_word_counts: List[int]
43
+ """List of expected word counts for each chapter in the novel. should be the same length as chapter_synopses."""
44
+
45
+ def model_post_init(self, context: Any, /) -> None:
46
+ """Make sure that the chapter expected word counts are aligned with the chapter synopses."""
47
+ if len(self.chapter_synopses) != len(self.chapter_expected_word_counts):
48
+ if self.chapter_expected_word_counts:
49
+ logger.warn(
50
+ "Chapter expected word counts are not aligned with chapter synopses, using the last valid one to fill the rest."
51
+ )
52
+ # If word counts are not aligned, copy the last valid chapter's word count
53
+ last_valid_wc = self.chapter_expected_word_counts[-1]
54
+ self.chapter_expected_word_counts.extend(
55
+ [last_valid_wc] * (len(self.chapter_synopses) - len(self.chapter_expected_word_counts))
56
+ )
57
+ else:
58
+ logger.warn("No chapter expected word counts provided, using the expected word count to fill the list.")
59
+ # If the word count list is totally empty, distribute the expected word count evenly
60
+ avg_wc = self.expected_word_count // len(self.chapter_synopses)
61
+ self.chapter_expected_word_counts = [avg_wc] * len(self.chapter_synopses)
62
+
63
+
64
+ class Chapter(SketchedAble, PersistentAble, Titled, WordCount):
65
+ """A chapter in a novel."""
66
+
67
+ content: str
68
+ """The content of the chapter."""
69
+
70
+ def to_xhtml(self) -> str:
71
+ """Convert the chapter to XHTML format."""
72
+ return TEMPLATE_MANAGER.render_template(novel_config.render_chapter_xhtml_template, self.model_dump())
73
+
74
+ @property
75
+ def exact_word_count(self) -> int:
76
+ """Calculate the exact word count of the chapter."""
77
+ return word_count(self.content)
78
+
79
+
80
+ class Novel(SketchedAble, PersistentAble, Titled, WordCount):
81
+ """A novel."""
82
+
83
+ synopsis: str
84
+ """A summary of the novel's plot."""
85
+ chapters: List[Chapter]
86
+ """List of chapters in the novel."""
87
+
88
+ @property
89
+ def exact_word_count(self) -> int:
90
+ """Calculate the exact word count of the novel."""
91
+ return sum(chapter.exact_word_count for chapter in self.chapters)
92
+
93
+ @property
94
+ def word_count_compliance_ratio(self) -> float:
95
+ """Calculate the compliance ratio of the novel's word count."""
96
+ return self.exact_word_count / self.expected_word_count
@@ -0,0 +1,76 @@
1
+ """This module defines the core data structures for narrative scenes and scripts.
2
+
3
+ Together, these classes form a foundation for creating structured yet flexible narrative content.
4
+ """
5
+
6
+ from typing import Any, ClassVar, Dict, List, Self
7
+
8
+ from fabricatio_capabilities.models.generic import AsPrompt, PersistentAble, WordCount
9
+ from fabricatio_core.models.generic import SketchedAble, Titled
10
+ from pydantic import Field
11
+
12
+ from fabricatio_novel.config import novel_config
13
+
14
+
15
+ class Scene(PersistentAble, SketchedAble, WordCount):
16
+ """The most basic narrative unit."""
17
+
18
+ expected_word_count: int
19
+ """Expected word count when writing the content that the Scene is narrating."""
20
+
21
+ tags: List[str]
22
+ """free-form semantic labels for filtering, grouping, or post-processing."""
23
+
24
+ prompt: str
25
+ """natural language guidance for tone, style, or constraint."""
26
+
27
+ narrative: str
28
+ """dialogue, description, log, poem, monologue, etc."""
29
+
30
+ def append_prompt(self, prompt: str) -> Self:
31
+ """Add a prompt to the scene.
32
+
33
+ Args:
34
+ prompt (str): The prompt to add.
35
+ """
36
+ self.prompt += f"\n{prompt}"
37
+ return self
38
+
39
+
40
+ class Script(SketchedAble, PersistentAble, Titled, AsPrompt, WordCount):
41
+ """A sequence of scenes forming a cohesive narrative unit especially for a novel chapter."""
42
+
43
+ title: str = Field(examples=["Ch1: A Chapter Title For Example", "Ch1: 一个示例章节标题"])
44
+ """Title of the chapter."""
45
+
46
+ expected_word_count: int
47
+ """Expected word count for this chapter."""
48
+
49
+ global_prompt: str
50
+ """global writing guidance applied to all scenes."""
51
+
52
+ scenes: List[Scene]
53
+ """Ordered list of scenes. Must contain at least one scene. Sequence implies narrative flow."""
54
+
55
+ rendering_template: ClassVar[str] = novel_config.render_script_template
56
+
57
+ def _as_prompt_inner(self) -> Dict[str, str] | Dict[str, Any] | Any:
58
+ return self.model_dump()
59
+
60
+ def append_global_prompt(self, prompt: str) -> Self:
61
+ """Add a global prompt to the script.
62
+
63
+ Args:
64
+ prompt (str): The global prompt to add.
65
+ """
66
+ self.global_prompt += f"\n{prompt}"
67
+ return self
68
+
69
+ def set_expected_word_count(self, word_count: int) -> Self:
70
+ """Set the expected word count for the script.
71
+
72
+ Args:
73
+ word_count (int): The expected word count.
74
+ """
75
+ self.expected_word_count = word_count
76
+ return self
File without changes
@@ -0,0 +1,120 @@
1
+ """Rust bindings for the Rust API of fabricatio-novel."""
2
+
3
+ from pathlib import Path
4
+ from typing import Self
5
+
6
+ class NovelBuilder:
7
+ """A Python-exposed builder for creating EPUB novels."""
8
+
9
+ def __init__(self) -> None:
10
+ """Creates a new uninitialized NovelBuilder instance."""
11
+
12
+ def new_novel(self) -> Self:
13
+ """Initializes a new EPUB novel builder.
14
+
15
+ Raises:
16
+ RuntimeError: If initialization fails.
17
+ """
18
+
19
+ def set_title(self, title: str) -> Self:
20
+ """Sets the novel title.
21
+
22
+ Raises:
23
+ RuntimeError: If novel is not initialized.
24
+ """
25
+ def add_author(self, author: str) -> Self:
26
+ """Adds an author to the novel metadata.
27
+
28
+ Raises:
29
+ RuntimeError: If novel is not initialized.
30
+ """
31
+
32
+ def add_chapter(self, title: str, content: str) -> Self:
33
+ """Adds a chapter with given title and content.
34
+
35
+ Raises:
36
+ RuntimeError: If novel is not initialized or chapter creation fails.
37
+ """
38
+
39
+ def set_description(self, description: str) -> Self:
40
+ """Sets the novel description.
41
+
42
+ Raises:
43
+ RuntimeError: If novel is not initialized.
44
+ """
45
+
46
+ def add_cover_image(self, path: str | Path, source: str | Path) -> Self:
47
+ """Adds a cover image from the given file path.
48
+
49
+ Args:
50
+ path: Path inside EPUB where image will be stored (e.g., "cover.png").
51
+ source: Filesystem path to the image file to read.
52
+
53
+ Raises:
54
+ RuntimeError: If novel not initialized, file read fails, or adding image fails.
55
+ """
56
+
57
+ def add_metadata(self, key: str, value: str) -> Self:
58
+ """Adds custom metadata key-value pair to the novel.
59
+
60
+ Raises:
61
+ RuntimeError: If novel is not initialized or metadata is invalid.
62
+ """
63
+
64
+ def add_css(self, css: str) -> Self:
65
+ """Adds custom CSS to the novel.
66
+
67
+ Args:
68
+ css: A string containing the CSS content to be added.
69
+
70
+ Raises:
71
+ RuntimeError: If novel is not initialized or CSS addition fails.
72
+ """
73
+
74
+ def add_resource(self, path: str | Path, source: str | Path) -> Self:
75
+ """Add a resource to the EPUB.
76
+
77
+ Args:
78
+ path: Internal EPUB path (e.g. 'images/cover.jpg').
79
+ source: Filesystem path to read from.
80
+
81
+ Returns:
82
+ Self for chaining.
83
+ """
84
+
85
+ def add_font(self, font_family: str, source: str | Path) -> Self:
86
+ """Embed a font and add @font-face CSS rule.
87
+
88
+ Font saved as 'fonts/{font_family}.ttf'.
89
+
90
+ Args:
91
+ font_family: Name used in CSS and filename.
92
+ source: TTF font file on disk.
93
+
94
+ Returns:
95
+ Self for chaining.
96
+ """
97
+
98
+ def add_inline_toc(self) -> Self:
99
+ """Enables inline table of contents generation.
100
+
101
+ Raises:
102
+ RuntimeError: If novel is not initialized.
103
+ """
104
+
105
+ def export(self, path: str | Path) -> Self:
106
+ """Exports the built novel to the specified file path.
107
+
108
+ Raises:
109
+ RuntimeError: If novel not initialized, generation fails, or file write fails.
110
+ """
111
+
112
+ def text_to_xhtml_paragraphs(source: str) -> str:
113
+ """Converts plain text to XHTML paragraphs.
114
+
115
+ Args:
116
+ source: Plain text to convert.
117
+
118
+ Returns:
119
+ XHTML string with paragraphs.
120
+ """
@@ -0,0 +1 @@
1
+ """Workflows defined in fabricatio-novel."""
@@ -0,0 +1,135 @@
1
+ """This module defines various workflows for novel generation, from full pipelines to granular steps.
2
+
3
+ Each workflow is designed for specific use cases: full generation, debugging, component regeneration, validation, etc.
4
+ """
5
+
6
+ from fabricatio_core.utils import cfg
7
+
8
+ cfg("fabricatio_actions", feats=["workflows"])
9
+ from pathlib import Path
10
+
11
+ from fabricatio_actions.actions.output import PersistentAll
12
+ from fabricatio_core import WorkFlow
13
+
14
+ from fabricatio_novel.actions.novel import (
15
+ AssembleNovelFromComponents,
16
+ DumpNovel,
17
+ GenerateChaptersFromScripts,
18
+ GenerateCharactersFromDraft,
19
+ GenerateNovel, # One-step full pipeline
20
+ GenerateNovelDraft,
21
+ GenerateScriptsFromDraftAndCharacters,
22
+ ValidateNovel,
23
+ )
24
+
25
+ # ==============================
26
+ # 🚀 One-Step Full Novel Generation (Existing, standardized)
27
+ # ==============================
28
+ WriteNovelWorkflow = WorkFlow(
29
+ name="WriteNovelWorkflow",
30
+ description="Generate and dump a novel from outline in one go.",
31
+ steps=(GenerateNovel, DumpNovel().to_task_output(), PersistentAll),
32
+ )
33
+ """Generate a novel from outline and dump it to file."""
34
+
35
+ # ==============================
36
+ # 🧩 Step-by-Step Debug Workflow (Recommended for development)
37
+ # ==============================
38
+ DebugNovelWorkflow = WorkFlow(
39
+ name="DebugNovelWorkflow",
40
+ description="Step-by-step novel generation for inspection and debugging.",
41
+ steps=(
42
+ GenerateNovelDraft,
43
+ PersistentAll,
44
+ GenerateCharactersFromDraft,
45
+ PersistentAll,
46
+ GenerateScriptsFromDraftAndCharacters,
47
+ PersistentAll,
48
+ GenerateChaptersFromScripts,
49
+ PersistentAll,
50
+ AssembleNovelFromComponents,
51
+ DumpNovel().to_task_output(),
52
+ PersistentAll,
53
+ ),
54
+ )
55
+ """Use this workflow to debug each stage of novel generation."""
56
+
57
+
58
+ # ==============================
59
+ # 🎭 Generate Characters Only (For character design phase)
60
+ # ==============================
61
+ GenerateOnlyCharactersWorkflow = WorkFlow(
62
+ name="GenerateOnlyCharactersWorkflow",
63
+ description="Generate character cards from a given novel draft.",
64
+ steps=(
65
+ GenerateNovelDraft,
66
+ GenerateCharactersFromDraft,
67
+ PersistentAll,
68
+ ),
69
+ )
70
+ """Useful for iterating on character design before full generation."""
71
+
72
+
73
+ # ==============================
74
+ # 📖 Rewrite Chapters Only (Reuse scripts + characters → regenerate prose)
75
+ # ==============================
76
+ RewriteChaptersOnlyWorkflow = WorkFlow(
77
+ name="RewriteChaptersOnlyWorkflow",
78
+ description="Regenerate chapter contents from existing scripts and characters.",
79
+ steps=(
80
+ GenerateChaptersFromScripts, # expects draft, scripts, characters in context
81
+ AssembleNovelFromComponents,
82
+ DumpNovel().to_task_output(),
83
+ PersistentAll,
84
+ ),
85
+ )
86
+ """Use when you want to rewrite chapter prose without changing plot or characters."""
87
+
88
+
89
+ # ==============================
90
+ # ✅ Validated Full Pipeline (Production-grade with quality checks)
91
+ # ==============================
92
+ ValidatedNovelWorkflow = WorkFlow(
93
+ name="ValidatedNovelWorkflow",
94
+ description="Generate novel with post-generation validation for quality control.",
95
+ steps=(
96
+ GenerateNovel,
97
+ ValidateNovel, # Halts or warns if validation fails (depends on engine)
98
+ DumpNovel().to_task_output(),
99
+ PersistentAll,
100
+ ),
101
+ )
102
+ """Ideal for production: ensures minimum chapters, word count, and compliance ratio."""
103
+
104
+
105
+ # ==============================
106
+ # 🔄 Regenerate with New Characters (A/B test character impact)
107
+ # ==============================
108
+ RegenerateWithNewCharactersWorkflow = WorkFlow(
109
+ name="RegenerateWithNewCharactersWorkflow",
110
+ description="Reuse existing draft but regenerate story with new characters.",
111
+ steps=(
112
+ GenerateNovelDraft, # Or inject pre-existing draft via context
113
+ GenerateCharactersFromDraft, # May yield different characters
114
+ GenerateScriptsFromDraftAndCharacters,
115
+ GenerateChaptersFromScripts,
116
+ AssembleNovelFromComponents,
117
+ DumpNovel(output_path=Path("output_with_new_characters.epub")).to_task_output(),
118
+ PersistentAll,
119
+ ),
120
+ )
121
+ """Use to explore how different character sets affect narrative outcomes."""
122
+
123
+
124
+ # ==============================
125
+ # 💾 Dump Only Workflow (For pre-generated Novel objects)
126
+ # ==============================
127
+ DumpOnlyWorkflow = WorkFlow(
128
+ name="DumpOnlyWorkflow",
129
+ description="Only dump an existing Novel object to file (no generation).",
130
+ steps=(
131
+ DumpNovel,
132
+ PersistentAll,
133
+ ),
134
+ )
135
+ """Use when Novel is pre-generated or loaded from DB/cache."""
@@ -0,0 +1,88 @@
1
+ Metadata-Version: 2.4
2
+ Name: fabricatio-novel
3
+ Version: 0.1.1
4
+ Classifier: License :: OSI Approved :: MIT License
5
+ Classifier: Programming Language :: Python :: 3.12
6
+ Classifier: Programming Language :: Python :: 3.13
7
+ Classifier: Programming Language :: Python :: Implementation :: CPython
8
+ Classifier: Typing :: Typed
9
+ Requires-Dist: fabricatio-character
10
+ Requires-Dist: fabricatio-core
11
+ Requires-Dist: pydantic>=2.11.9
12
+ Requires-Dist: fabricatio-novel[workflows,cli] ; extra == 'full'
13
+ Requires-Dist: fabricatio-actions ; extra == 'workflows'
14
+ Requires-Dist: fabricatio-novel[workflows] ; extra == 'cli'
15
+ Requires-Dist: questionary>=2.1.0 ; extra == 'cli'
16
+ Requires-Dist: typer-slim[standard]>=0.15.2 ; extra == 'cli'
17
+ Provides-Extra: full
18
+ Provides-Extra: workflows
19
+ Provides-Extra: cli
20
+ Summary: An extension of fabricatio
21
+ Author-email: Whth <zettainspector@foxmail.com>
22
+ License-Expression: MIT
23
+ Requires-Python: >=3.12, <3.14
24
+ Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
25
+ Project-URL: Homepage, https://github.com/Whth/fabricatio
26
+ Project-URL: Repository, https://github.com/Whth/fabricatio
27
+ Project-URL: Issues, https://github.com/Whth/fabricatio/issues
28
+
29
+ # `fabricatio-novel`
30
+
31
+ [MIT](https://img.shields.io/badge/license-MIT-blue.svg)
32
+ ![Python Versions](https://img.shields.io/pypi/pyversions/fabricatio-novel)
33
+ [![PyPI Version](https://img.shields.io/pypi/v/fabricatio-novel)](https://pypi.org/project/fabricatio-novel/)
34
+ [![PyPI Downloads](https://static.pepy.tech/badge/fabricatio-novel/week)](https://pepy.tech/projects/fabricatio-novel)
35
+ [![PyPI Downloads](https://static.pepy.tech/badge/fabricatio-novel)](https://pepy.tech/projects/fabricatio-novel)
36
+ [![Bindings: PyO3](https://img.shields.io/badge/bindings-pyo3-green)](https://github.com/PyO3/pyo3)
37
+ [![Build Tool: uv + maturin](https://img.shields.io/badge/built%20with-uv%20%2B%20maturin-orange)](https://github.com/astral-sh/uv)
38
+
39
+
40
+
41
+ An extension of fabricatio.
42
+
43
+ ---
44
+
45
+ ## 📦 Installation
46
+
47
+
48
+ This package is part of the `fabricatio` monorepo and can be installed as an optional dependency:
49
+
50
+ ```bash
51
+ pip install fabricatio[novel]
52
+
53
+ # or with uv
54
+ # uv pip install fabricatio[novel]
55
+ ```
56
+
57
+ Or install `fabricatio-diff` along with all other components of `fabricatio`:
58
+
59
+ ```bash
60
+ pip install fabricatio[full]
61
+
62
+ # or with uv
63
+ # uv pip install fabricatio[full]
64
+ ```
65
+
66
+ ## 🔍 Overview
67
+
68
+ Provides essential tools for:
69
+
70
+ ...
71
+
72
+
73
+
74
+ ## 🧩 Key Features
75
+
76
+ ...
77
+
78
+
79
+ ## 🔗 Dependencies
80
+
81
+ Core dependencies:
82
+
83
+ - `fabricatio-core` - Core interfaces and utilities
84
+ ...
85
+
86
+ ## 📄 License
87
+
88
+ This project is licensed under the MIT License.
@@ -0,0 +1,19 @@
1
+ fabricatio_novel-0.1.1.dist-info/METADATA,sha256=NPEHr0pTAgav75aj_sgcjdQzruZvedzs6RH8mUtHfDc,2560
2
+ fabricatio_novel-0.1.1.dist-info/WHEEL,sha256=dMy2teuP-PSJJ9D8IjLXBMRWYNMh-F_X_I-9plY-tIo,108
3
+ fabricatio_novel-0.1.1.dist-info/entry_points.txt,sha256=Yv1aMQSZeRVHkj1KGkwUrYmNBdIt0F0a_rCdW0KxUco,49
4
+ fabricatio_novel/__init__.py,sha256=2gnAk79LxfdFJxrioPkl-DyZJ4mm9CnYVZNz7o7iz4g,34
5
+ fabricatio_novel/actions/__init__.py,sha256=HBG2V0J0RgYrqWygcA5VV39y33JwRHyeYMTxnrNJkZs,43
6
+ fabricatio_novel/actions/novel.py,sha256=Jnrz9D2CYrLkSAVXiNfW7C8hKSdMOnI7GT3Poj-5BEY,11191
7
+ fabricatio_novel/capabilities/__init__.py,sha256=fDDWl7Sskw3A3344Utctn6rb7513caADquP2nv2CQ3c,48
8
+ fabricatio_novel/capabilities/novel.py,sha256=3Y6dCbSBNMoDP1cwvU0l50IMqDMlskWsI_Mhq_7p0P8,10148
9
+ fabricatio_novel/cli.py,sha256=AbFeavbJ9HMYzMq8sLUCwzvifW8vvpkyI3xMCUsHy0w,5104
10
+ fabricatio_novel/config.py,sha256=tquQs21El4YrEx5Hmxdzx5aMAV8ruKvTXBnHQ0n1M_s,1005
11
+ fabricatio_novel/models/__init__.py,sha256=spyN6xd8hHPIuFhU_QDAxMDapD4yO5NQjiXqOAeY2fA,42
12
+ fabricatio_novel/models/novel.py,sha256=AppZ9_4woxh5-fzvh7EyPcNDAAxfPUmhDVXCMwmDhYc,4128
13
+ fabricatio_novel/models/scripting.py,sha256=uuCG8ge0kHksimz3kO5nyQnrKfrzXoFk-qRmJRSG9CE,2453
14
+ fabricatio_novel/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
15
+ fabricatio_novel/rust.cpython-313-x86_64-linux-gnu.so,sha256=22a-xgiJCAIQmQS-exM-QxOp_rN_M_w7Bu8pVpkj-0c,7162360
16
+ fabricatio_novel/rust.pyi,sha256=bPhZMS5Zdkvh1o2ghaI2RiUWqEWZ3iXVxNjzHfgjUkc,3389
17
+ fabricatio_novel/workflows/__init__.py,sha256=NGOA4UgpPkUjKtXrzh4JDhRb6P7VqpCUqzxh-eduj9Y,45
18
+ fabricatio_novel/workflows/novel.py,sha256=rKuuqnMptq9F4nbOOZJZU_O1GxI4UlJJvliFcEoDji0,4632
19
+ fabricatio_novel-0.1.1.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: maturin (1.9.4)
3
+ Root-Is-Purelib: false
4
+ Tag: cp313-cp313-manylinux_2_34_x86_64
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ fanvl=fabricatio_novel.cli:app