new editor features
This commit is contained in:
35
main.py
35
main.py
@@ -106,7 +106,8 @@ def process_book(bp, folder, context="", resume=False):
|
|||||||
session_chapters = 0
|
session_chapters = 0
|
||||||
session_time = 0
|
session_time = 0
|
||||||
|
|
||||||
for i in range(len(ms), len(chapters)):
|
i = len(ms)
|
||||||
|
while i < len(chapters):
|
||||||
ch_start = time.time()
|
ch_start = time.time()
|
||||||
ch = chapters[i]
|
ch = chapters[i]
|
||||||
|
|
||||||
@@ -165,6 +166,38 @@ def process_book(bp, folder, context="", resume=False):
|
|||||||
with open(chars_track_path, "w") as f: json.dump(tracking['characters'], f, indent=2)
|
with open(chars_track_path, "w") as f: json.dump(tracking['characters'], f, indent=2)
|
||||||
with open(warn_track_path, "w") as f: json.dump(tracking.get('content_warnings', []), f, indent=2)
|
with open(warn_track_path, "w") as f: json.dump(tracking.get('content_warnings', []), f, indent=2)
|
||||||
|
|
||||||
|
# --- DYNAMIC PACING CHECK ---
|
||||||
|
remaining = chapters[i+1:]
|
||||||
|
if remaining:
|
||||||
|
pacing = story.check_pacing(bp, summary, txt, ch, remaining, folder)
|
||||||
|
if pacing and pacing.get('status') == 'add_bridge':
|
||||||
|
new_data = pacing.get('new_chapter', {})
|
||||||
|
new_ch = {
|
||||||
|
"chapter_number": ch['chapter_number'] + 1,
|
||||||
|
"title": new_data.get('title', 'Bridge Chapter'),
|
||||||
|
"pov_character": new_data.get('pov_character', ch.get('pov_character')),
|
||||||
|
"pacing": "Slow",
|
||||||
|
"estimated_words": 1500,
|
||||||
|
"beats": new_data.get('beats', [])
|
||||||
|
}
|
||||||
|
chapters.insert(i+1, new_ch)
|
||||||
|
# Renumber subsequent chapters
|
||||||
|
for k in range(i+1, len(chapters)): chapters[k]['chapter_number'] = k + 1
|
||||||
|
|
||||||
|
with open(chapters_path, "w") as f: json.dump(chapters, f, indent=2)
|
||||||
|
utils.log("ARCHITECT", f" -> ⚠️ Pacing Intervention: Added bridge chapter '{new_ch['title']}' to fix rushing.")
|
||||||
|
|
||||||
|
elif pacing and pacing.get('status') == 'cut_next':
|
||||||
|
removed = chapters.pop(i+1)
|
||||||
|
# Renumber subsequent chapters
|
||||||
|
for k in range(i+1, len(chapters)): chapters[k]['chapter_number'] = k + 1
|
||||||
|
|
||||||
|
with open(chapters_path, "w") as f: json.dump(chapters, f, indent=2)
|
||||||
|
utils.log("ARCHITECT", f" -> ⚠️ Pacing Intervention: Removed redundant chapter '{removed['title']}'.")
|
||||||
|
|
||||||
|
# Increment loop
|
||||||
|
i += 1
|
||||||
|
|
||||||
duration = time.time() - ch_start
|
duration = time.time() - ch_start
|
||||||
session_chapters += 1
|
session_chapters += 1
|
||||||
session_time += duration
|
session_time += duration
|
||||||
|
|||||||
@@ -365,19 +365,23 @@ def evaluate_chapter_quality(text, chapter_title, model, folder):
|
|||||||
- Stilted Dialogue: Characters speaking in perfect paragraphs without interruptions, slang, or subtext.
|
- Stilted Dialogue: Characters speaking in perfect paragraphs without interruptions, slang, or subtext.
|
||||||
- White Room Syndrome: Dialogue occurring in a void without interaction with the setting/props.
|
- White Room Syndrome: Dialogue occurring in a void without interaction with the setting/props.
|
||||||
- "As You Know, Bob": Characters explaining things to each other that they both already know.
|
- "As You Know, Bob": Characters explaining things to each other that they both already know.
|
||||||
|
- Summary Mode: Summarizing conversation or action instead of dramatizing it (e.g. "They discussed the plan" vs writing the dialogue).
|
||||||
|
|
||||||
CRITERIA:
|
CRITERIA:
|
||||||
1. VOICE & TONE: Is the narrative voice distinct, or generic? Does it match the genre?
|
1. ENGAGEMENT & TENSION: Does the story grip the reader from the first line? Is there conflict or tension in every scene?
|
||||||
2. SHOW, DON'T TELL: Are emotions demonstrated through action/viscera, or summarized?
|
2. SCENE EXECUTION: Is the middle of the chapter fully fleshed out? Does it avoid "sagging" or summarizing key moments?
|
||||||
3. PACING: Does the scene drag? Is there conflict in every beat?
|
3. VOICE & TONE: Is the narrative voice distinct? Does it match the genre?
|
||||||
4. CHARACTER AGENCY: Do characters make choices, or do things just happen to them?
|
4. SENSORY IMMERSION: Does the text engage all five senses (smell, sound, touch, etc.)?
|
||||||
|
5. SHOW, DON'T TELL: Are emotions shown through physical reactions and subtext?
|
||||||
|
6. CHARACTER AGENCY: Do characters drive the plot through active choices?
|
||||||
|
7. PACING: Does the chapter feel rushed? Does the ending land with impact, or does it cut off too abruptly?
|
||||||
|
|
||||||
Rate on a scale of 1-10. (Be harsh. 10 is Pulitzer level. 6 is average. Anything below 8 needs work).
|
Rate on a scale of 1-10. (Be harsh. 10 is Pulitzer level. 6 is average. Anything below 8 needs work).
|
||||||
|
|
||||||
Return JSON: {{
|
Return JSON: {{
|
||||||
'score': int,
|
'score': int,
|
||||||
'critique': 'Detailed analysis of flaws, citing specific examples from the text.',
|
'critique': 'Detailed analysis of flaws, citing specific examples from the text.',
|
||||||
'actionable_feedback': 'List of 3-5 specific, ruthless instructions for the rewrite (e.g. "Cut the first 3 paragraphs", "Make the dialogue in the middle argument more aggressive", "Describe the smell of the room").'
|
'actionable_feedback': 'List of 3-5 specific, ruthless instructions for the rewrite (e.g. "Expand the middle dialogue", "Add sensory details about the rain", "Dramatize the argument instead of summarizing it").'
|
||||||
}}
|
}}
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
@@ -393,6 +397,53 @@ def evaluate_chapter_quality(text, chapter_title, model, folder):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
return 0, f"Evaluation error: {str(e)}"
|
return 0, f"Evaluation error: {str(e)}"
|
||||||
|
|
||||||
|
def check_pacing(bp, summary, last_chapter_text, last_chapter_data, remaining_chapters, folder):
|
||||||
|
utils.log("ARCHITECT", "Checking pacing and structure health...")
|
||||||
|
|
||||||
|
if not remaining_chapters:
|
||||||
|
return None
|
||||||
|
|
||||||
|
meta = bp.get('book_metadata', {})
|
||||||
|
genre = meta.get('genre', 'Fiction')
|
||||||
|
|
||||||
|
prompt = f"""
|
||||||
|
Act as a Senior Structural Editor.
|
||||||
|
We just finished Chapter {last_chapter_data['chapter_number']}: "{last_chapter_data['title']}".
|
||||||
|
|
||||||
|
STORY SO FAR (Summary):
|
||||||
|
{summary[-3000:]}
|
||||||
|
|
||||||
|
JUST WRITTEN (Last 2000 chars):
|
||||||
|
{last_chapter_text[-2000:]}
|
||||||
|
|
||||||
|
UPCOMING CHAPTERS (Next 3):
|
||||||
|
{json.dumps([c['title'] for c in remaining_chapters[:3]])}
|
||||||
|
|
||||||
|
TOTAL REMAINING: {len(remaining_chapters)} chapters.
|
||||||
|
|
||||||
|
ANALYSIS TASK:
|
||||||
|
Determine if the story is moving too fast (Rushed) or too slow (Dragging) based on the {genre} genre.
|
||||||
|
|
||||||
|
DECISION RULES:
|
||||||
|
- If the last chapter skipped over major emotional reactions, travel, or necessary setup -> ADD_BRIDGE.
|
||||||
|
- If the last chapter already covered the events of the NEXT chapter -> CUT_NEXT.
|
||||||
|
- If the pacing is fine -> OK.
|
||||||
|
|
||||||
|
RETURN JSON:
|
||||||
|
{{
|
||||||
|
"status": "ok" or "add_bridge" or "cut_next",
|
||||||
|
"reason": "Explanation...",
|
||||||
|
"new_chapter": {{ "title": "...", "beats": ["..."], "pov_character": "..." }} (Required if add_bridge)
|
||||||
|
}}
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
response = ai.model_logic.generate_content(prompt)
|
||||||
|
utils.log_usage(folder, "logic-pro", response.usage_metadata)
|
||||||
|
return json.loads(utils.clean_json(response.text))
|
||||||
|
except Exception as e:
|
||||||
|
utils.log("ARCHITECT", f"Pacing check failed: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
def create_initial_persona(bp, folder):
|
def create_initial_persona(bp, folder):
|
||||||
utils.log("SYSTEM", "Generating initial Author Persona based on genre/tone...")
|
utils.log("SYSTEM", "Generating initial Author Persona based on genre/tone...")
|
||||||
meta = bp.get('book_metadata', {})
|
meta = bp.get('book_metadata', {})
|
||||||
@@ -618,6 +669,7 @@ def write_chapter(chap, bp, folder, prev_sum, tracking=None, prev_content=None):
|
|||||||
3. SUBTEXT: Ensure dialogue implies meaning rather than stating it outright. People rarely say exactly what they mean.
|
3. SUBTEXT: Ensure dialogue implies meaning rather than stating it outright. People rarely say exactly what they mean.
|
||||||
4. GENRE CONSISTENCY: Ensure the tone matches {meta.get('genre', 'Fiction')}.
|
4. GENRE CONSISTENCY: Ensure the tone matches {meta.get('genre', 'Fiction')}.
|
||||||
5. SETTING INTERACTION: Ensure characters interact with their environment (props, weather, lighting) during dialogue.
|
5. SETTING INTERACTION: Ensure characters interact with their environment (props, weather, lighting) during dialogue.
|
||||||
|
6. DRAMATIZE, DON'T SUMMARIZE: Expand summarized moments into full scenes with dialogue and action. Ensure the scene feels "full" and immersive.
|
||||||
|
|
||||||
STORY SO FAR:
|
STORY SO FAR:
|
||||||
{prev_sum}
|
{prev_sum}
|
||||||
|
|||||||
@@ -459,6 +459,7 @@
|
|||||||
let activeInterval = null;
|
let activeInterval = null;
|
||||||
// Only auto-poll if we have a latest run
|
// Only auto-poll if we have a latest run
|
||||||
let currentRunId = {{ active_run.id if active_run else 'null' }};
|
let currentRunId = {{ active_run.id if active_run else 'null' }};
|
||||||
|
const initialRunStatus = "{{ active_run.status if active_run else '' }}";
|
||||||
|
|
||||||
function fetchLog() {
|
function fetchLog() {
|
||||||
if (!currentRunId) return;
|
if (!currentRunId) return;
|
||||||
@@ -509,8 +510,9 @@
|
|||||||
} else {
|
} else {
|
||||||
if (activeInterval) clearInterval(activeInterval);
|
if (activeInterval) clearInterval(activeInterval);
|
||||||
activeInterval = null;
|
activeInterval = null;
|
||||||
// Reload page on completion to show download buttons
|
|
||||||
if (data.status === 'completed' && !document.querySelector('.alert-success')) {
|
// Reload if we were polling (watched it finish) OR if page loaded as running but is now done
|
||||||
|
if (initialRunStatus === 'running' || initialRunStatus === 'queued') {
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -260,6 +260,7 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
const runId = {{ run.id }};
|
const runId = {{ run.id }};
|
||||||
|
const initialStatus = "{{ run.status }}";
|
||||||
const consoleEl = document.getElementById('console-log');
|
const consoleEl = document.getElementById('console-log');
|
||||||
const statusText = document.getElementById('status-text');
|
const statusText = document.getElementById('status-text');
|
||||||
const statusBar = document.getElementById('status-bar');
|
const statusBar = document.getElementById('status-bar');
|
||||||
@@ -297,6 +298,11 @@
|
|||||||
// Poll if running
|
// Poll if running
|
||||||
if (data.status === 'running' || data.status === 'queued') {
|
if (data.status === 'running' || data.status === 'queued') {
|
||||||
setTimeout(updateLog, 2000);
|
setTimeout(updateLog, 2000);
|
||||||
|
} else {
|
||||||
|
// If the run was active when we loaded the page, reload now that it's finished to show artifacts
|
||||||
|
if (initialStatus === 'running' || initialStatus === 'queued') {
|
||||||
|
window.location.reload();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(err => console.error(err));
|
.catch(err => console.error(err));
|
||||||
|
|||||||
Reference in New Issue
Block a user