New comparison feature.

This commit is contained in:
2026-02-04 21:21:57 -05:00
parent 48dca539cd
commit ca221f0fb3
5 changed files with 356 additions and 21 deletions

View File

@@ -66,6 +66,57 @@ def refresh_style_guidelines(model, folder=None):
utils.log("SYSTEM", f"Failed to refresh guidelines: {e}")
return current
def merge_selected_changes(original, draft, selected_keys):
"""Helper to merge specific fields from draft to original bible."""
# Sort keys to ensure deterministic order
def sort_key(k):
return [int(p) if p.isdigit() else p for p in k.split('.')]
selected_keys.sort(key=sort_key)
for key in selected_keys:
parts = key.split('.')
# Metadata: meta.title
if parts[0] == 'meta' and len(parts) == 2:
field = parts[1]
if field == 'tone':
original['project_metadata']['style']['tone'] = draft['project_metadata']['style']['tone']
elif field in original['project_metadata']:
original['project_metadata'][field] = draft['project_metadata'][field]
# Characters: char.0
elif parts[0] == 'char' and len(parts) >= 2:
idx = int(parts[1])
if idx < len(draft['characters']):
if idx < len(original['characters']):
original['characters'][idx] = draft['characters'][idx]
else:
original['characters'].append(draft['characters'][idx])
# Books: book.1.title
elif parts[0] == 'book' and len(parts) >= 2:
book_num = int(parts[1])
orig_book = next((b for b in original['books'] if b['book_number'] == book_num), None)
draft_book = next((b for b in draft['books'] if b['book_number'] == book_num), None)
if draft_book:
if not orig_book:
original['books'].append(draft_book)
original['books'].sort(key=lambda x: x.get('book_number', 999))
continue
if len(parts) == 2:
orig_book['title'] = draft_book['title']
orig_book['manual_instruction'] = draft_book['manual_instruction']
elif len(parts) == 4 and parts[2] == 'beat':
beat_idx = int(parts[3])
if beat_idx < len(draft_book['plot_beats']):
while len(orig_book['plot_beats']) <= beat_idx:
orig_book['plot_beats'].append("")
orig_book['plot_beats'][beat_idx] = draft_book['plot_beats'][beat_idx]
return original
def filter_characters(chars):
"""Removes placeholder characters generated by AI."""
blacklist = ['name', 'character name', 'role', 'protagonist', 'antagonist', 'love interest', 'unknown', 'tbd', 'todo', 'hero', 'villain', 'main character', 'side character']

View File

@@ -569,30 +569,65 @@ def refine_bible_route(id):
if is_project_locked(id):
flash("Project is locked. Clone it to make changes.")
return redirect(url_for('view_project', id=id))
instruction = request.form.get('instruction')
if not instruction:
flash("Instruction required.")
return redirect(url_for('view_project', id=id))
bible_path = os.path.join(proj.folder_path, "bible.json")
bible = utils.load_json(bible_path)
if bible:
try: ai.init_models()
except: pass
if not ai.model_logic:
flash("AI models not initialized.")
return redirect(url_for('view_project', id=id))
# Handle JSON request (AJAX) or Form request
data = request.json if request.is_json else request.form
instruction = data.get('instruction')
if not instruction:
return {"error": "Instruction required"}, 400
new_bible = story.refine_bible(bible, instruction, proj.folder_path)
if new_bible:
with open(bible_path, 'w') as f: json.dump(new_bible, f, indent=2)
source_type = data.get('source', 'original')
selected_keys = data.get('selected_keys')
if isinstance(selected_keys, str):
try: selected_keys = json.loads(selected_keys)
except: selected_keys = []
# Start Background Task
task = refine_bible_task(proj.folder_path, instruction, source_type, selected_keys)
return {"status": "queued", "task_id": task.id}
@app.route('/project/<int:id>/refine_bible/confirm', methods=['POST'])
@login_required
def confirm_bible_refinement(id):
proj = db.session.get(Project, id) or Project.query.get_or_404(id)
if proj.user_id != current_user.id: return "Unauthorized", 403
action = request.form.get('action')
draft_path = os.path.join(proj.folder_path, "bible_draft.json")
bible_path = os.path.join(proj.folder_path, "bible.json")
if action == 'accept_all':
if os.path.exists(draft_path):
shutil.move(draft_path, bible_path)
flash("Bible updated successfully.")
else:
flash("AI failed to update Bible. Check logs.")
flash("Draft expired or missing.")
elif action == 'accept_selected':
if os.path.exists(draft_path) and os.path.exists(bible_path):
selected_keys_json = request.form.get('selected_keys', '[]')
try:
selected_keys = json.loads(selected_keys_json)
draft = utils.load_json(draft_path)
original = utils.load_json(bible_path)
original = _merge_selected_changes(original, draft, selected_keys)
with open(bible_path, 'w') as f: json.dump(original, f, indent=2)
os.remove(draft_path) # Cleanup draft after merge
flash(f"Merged {len(selected_keys)} changes into Bible.")
except Exception as e:
flash(f"Merge failed: {e}")
else:
flash("Files missing.")
elif action == 'decline':
if os.path.exists(draft_path):
os.remove(draft_path)
flash("Changes discarded.")
return redirect(url_for('view_project', id=id))

View File

@@ -367,4 +367,45 @@ def rewrite_chapter_task(run_id, project_path, book_folder, chap_num, instructio
return False
except Exception as e:
utils.log("ERROR", f"Rewrite task exception for run {run_id}/{book_folder}: {e}")
return False
@huey.task()
def refine_bible_task(project_path, instruction, source_type, selected_keys=None):
"""
Background task to refine the Bible.
Handles partial merging of selected keys into a temp base before refinement.
"""
try:
bible_path = os.path.join(project_path, "bible.json")
draft_path = os.path.join(project_path, "bible_draft.json")
base_bible = utils.load_json(bible_path)
if not base_bible: return False
# If refining from draft, load it
if source_type == 'draft' and os.path.exists(draft_path):
draft_bible = utils.load_json(draft_path)
# If user selected specific changes, merge them into the base
# This creates a "Proposed State" to refine further, WITHOUT modifying bible.json
if selected_keys and draft_bible:
base_bible = story.merge_selected_changes(base_bible, draft_bible, selected_keys)
elif draft_bible:
# If no specific keys but source is draft, assume we refine the whole draft
base_bible = draft_bible
ai.init_models()
# Run AI Refinement
new_bible = story.refine_bible(base_bible, instruction, project_path)
if new_bible:
# Save to draft file (Overwrite previous draft)
with open(draft_path, 'w') as f: json.dump(new_bible, f, indent=2)
return True
return False
except Exception as e:
utils.log("ERROR", f"Bible refinement task failed: {e}")
return False