New comparison feature.
This commit is contained in:
@@ -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']
|
||||
|
||||
@@ -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))
|
||||
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user