File size: 6,974 Bytes
fb856e1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
from agents import Agent, WebSearchTool, trace, Runner, gen_trace_id, function_tool
from agents.model_settings import ModelSettings
from pydantic import BaseModel, Field
from dotenv import load_dotenv
import asyncio
import os
import itertools  # Needed for loading animation
from typing import Dict
from IPython.display import display, Markdown

load_dotenv(override=True)

# Async loading indicator that runs until the event is set
async def show_loading_indicator(done_event):
    for dots in itertools.cycle(['', '.', '..', '...']):
        if done_event.is_set():
            break
        print(f'\rGenerating{dots}', end='', flush=True)
        await asyncio.sleep(0.5)
    print('\rDone generating!     ')  # Clear the line when done

def prompt_with_default(prompt_text, default_value=None, cast_type=str):
    user_input = input(f"{prompt_text} ")
    if user_input.strip() == "":
        return default_value
    try:
        return cast_type(user_input)
    except ValueError:
        print(f"Invalid input. Using default: {default_value}")
        return default_value

def get_user_inputs():
    # 1. Novel genre
    genre = prompt_with_default("Novel genre (press Enter for default - teen mystery):", "teen mystery")

    # 2. General plot
    plot = input("\nGeneral plot (Enter for auto-generated plot): ").strip()
    if not plot:
        plot = "Auto-Generated Plot"

    # 3. Title
    title = input("\nTitle (Enter for auto-generated title): ").strip()
    if not title:
        title = "Auto-Generated Title"

    # 4. Number of pages
    num_pages = prompt_with_default("\nNumber of pages in novel (Enter for default - 90 pages):", 90, int)
    num_words = num_pages * 275

    # 5. Number of chapters
    num_chapters = prompt_with_default("\nNumber of chapters (Enter for default - 15):", 15, int)

    # 6. Max AI tokens
    while True:
        max_tokens_input = input(
            "\nMaximum AI tokens to use, after which novel \n"
            "generation will fail (about 200,000 tokens for 90): "
        ).strip()
        try:
            max_tokens = int(max_tokens_input)
            if max_tokens <= 0:
                print("Please enter a positive integer.")
                continue

            if max_tokens > 300000:
                print(f"\n⚠️  You entered {max_tokens:,} tokens, which is quite high and may be expensive.")
                confirm = input("Are you sure you want to use this value? (Yes or No): ").strip().lower()
                if confirm != "yes":
                    print("Okay, let's try again.\n")
                    continue  # Ask again

            break  # Valid and confirmed
        except ValueError:
            print("Please enter a valid integer.")
    return genre, title, num_pages, num_words, num_chapters, plot, max_tokens

async def generate_novel(genre, title, num_pages, num_words, num_chapters, plot, max_tokens):
    # Print collected inputs for confirmation (optional)
    print("\nCOLLECTED NOVEL CONFIGURATION:\n")
    print(f"Genre: {genre}")
    print(f"Plot: {plot}")
    print(f"Title: {title}")
    print(f"Pages: {num_pages}")
    print(f"Chapters: {num_chapters}")
    print(f"Max Tokens: {max_tokens}")

    print("\nAwesome, now we'll generate your novel!")

    INSTRUCTIONS = f"You are a fiction author assistant. You will use user-provided parameters, \
    or default parameters, to generate a creative and engaging novel. \
    Do not perform web searches. Focus entirely on imaginative, coherent, and emotionally engaging content. \
    Your output should read like a real novel, vivid, descriptive, and character-driven. \
    \
    If the user input plot is \"Auto-Generated Plot\" then you should generate an interesting plot for the novel \
    based on the genre, otherwise use the plot provided by the user. \
    \
    If the user input title is \"Auto-Generated Title\" then you should generate an interesting title \
    based on the genre and plot, otherwise use the title provided by the user. \
    \
    The genre of the novel is {genre}. The plot of the novel is {plot}. The title of the novel is {title}. \
    You should generate a novel that is {num_pages} pages long. Ensure you do not abruptly end the novel \
    just to match the specified number of pages. So ensure the story naturally concludes leading up to the end. \
    The novel should be broken up into {num_chapters} chapters. Each chapter should develop the characters and \
    the story in an interesting and engaging way. \
    \
    Do not include any markdown or formatting symbols (e.g., ###, ---, **, etc.). \
    Use plain text only: start with the title, followed by chapter titles and their respective story content. \
    Do not include a conclusion or author notes at the end. End the story when the final chapter ends naturally. \
    \
    The story should contain approximately {num_words} words to match a target of {num_pages} standard paperback pages. \
    Each chapter should contribute proportionally to the total word count. \
    Continue generating story content until the target word count is reached or slightly exceeded. \
    Do not summarize or compress events to shorten the story."

    search_agent = Agent(
        name="Novel Generator Agent",
        instructions=INSTRUCTIONS,
        model="gpt-4o-mini",
        model_settings=ModelSettings(
            temperature=0.8,
            top_p=0.9,
            frequency_penalty=0.5,
            presence_penalty=0.6,
            max_tokens=max_tokens
        )
    )

    message = f"Generate a {genre} novel titled '{title}' with {num_pages} pages."

    with trace("Search"):
        result = await Runner.run(
            search_agent, 
            message
        )

    return result.final_output

# Your agent call with loading indicator
async def main():
    done_event = asyncio.Event()
    loader_task = asyncio.create_task(show_loading_indicator(done_event))

    # Run the agent
    genre, title, num_pages, num_words, num_chapters, plot, max_tokens = get_user_inputs()

    result = await generate_novel(
        genre, title, num_pages, num_words, num_chapters, plot, max_tokens
    )

    # Signal that loading is done
    done_event.set()
    await loader_task  # Let it finish cleanly

    # Output result to file
    lines = result.strip().splitlines()
    generated_title = "untitled_novel"
    for line in lines:
        if line.strip():  # skip empty lines
            generated_title = line.strip()
            break

    # Sanitize title for filename
    filename_safe_title = ''.join(c if c.isalnum() or c in (' ', '_', '-') else '_' for c in generated_title).strip().replace(' ', '_')
    output_path = os.path.abspath(f"novel_{filename_safe_title}.txt")

    # Save to file
    with open(output_path, "w", encoding="utf-8") as f:
        f.write(result)

    # Show full path
    print(f"\n📘 Novel saved to: {output_path}")

if __name__ == "__main__":
    asyncio.run(main())