import json import os import random import time import config from modules import ai from . import utils def enrich(bp, folder, context=""): utils.log("ENRICHER", "Fleshing out details from description...") # If book_metadata is missing, create empty dict so AI can fill it if 'book_metadata' not in bp: bp['book_metadata'] = {} if 'characters' not in bp: bp['characters'] = [] if 'plot_beats' not in bp: bp['plot_beats'] = [] prompt = f""" You are a Creative Director. The user has provided a minimal description. You must build a full Book Bible. USER DESCRIPTION: "{bp.get('manual_instruction', 'A generic story')}" CONTEXT (Sequel): {context} TASK: 1. Generate a catchy Title. 2. Define the Genre and Tone. 3. Determine the Time Period (e.g. "Modern", "1920s", "Sci-Fi Future"). 4. Define Formatting Rules for text messages, thoughts, and chapter headers. 5. Create Protagonist and Antagonist/Love Interest. - IF SEQUEL: Decide if we continue with previous protagonists or shift to side characters based on USER DESCRIPTION. - IF NEW CHARACTERS: Create them. - IF RETURNING: Reuse details from CONTEXT. 6. Outline 5-7 core Plot Beats. 7. Define a 'structure_prompt' describing the narrative arc (e.g. "Hero's Journey", "3-Act Structure", "Detective Procedural"). RETURN JSON in this EXACT format: {{ "book_metadata": {{ "title": "Book Title", "genre": "Genre", "content_warnings": ["Violence", "Major Character Death"], "structure_prompt": "...", "style": {{ "tone": "Tone", "time_period": "Modern", "formatting_rules": ["Chapter Headers: Number + Title", "Text Messages: Italic", "Thoughts: Italic"] }} }}, "characters": [ {{ "name": "Name", "role": "Role", "description": "Description", "key_events": ["Planned injury in Act 2"] }} ], "plot_beats": [ "Beat 1", "Beat 2", "..." ] }} """ try: # Merge AI response with existing data (don't overwrite if user provided specific keys) response = ai.model_logic.generate_content(prompt) utils.log_usage(folder, "logic-pro", response.usage_metadata) response_text = response.text cleaned_json = utils.clean_json(response_text) ai_data = json.loads(cleaned_json) # Smart Merge: Only fill missing fields if 'book_metadata' not in bp: bp['book_metadata'] = {} if 'title' not in bp['book_metadata']: bp['book_metadata']['title'] = ai_data.get('book_metadata', {}).get('title') if 'structure_prompt' not in bp['book_metadata']: bp['book_metadata']['structure_prompt'] = ai_data.get('book_metadata', {}).get('structure_prompt') if 'content_warnings' not in bp['book_metadata']: bp['book_metadata']['content_warnings'] = ai_data.get('book_metadata', {}).get('content_warnings', []) # Merge Style (Flexible) if 'style' not in bp['book_metadata']: bp['book_metadata']['style'] = {} # Handle AI returning legacy keys or new style key source_style = ai_data.get('book_metadata', {}).get('style', {}) for k, v in source_style.items(): if k not in bp['book_metadata']['style']: bp['book_metadata']['style'][k] = v if 'characters' not in bp or not bp['characters']: bp['characters'] = ai_data.get('characters', []) if 'plot_beats' not in bp or not bp['plot_beats']: bp['plot_beats'] = ai_data.get('plot_beats', []) return bp except Exception as e: utils.log("ENRICHER", f"Enrichment failed: {e}") return bp def plan_structure(bp, folder): utils.log("ARCHITECT", "Creating structure...") if 'plot_outline' in bp and isinstance(bp['plot_outline'], dict): po = bp['plot_outline'] if 'beats' in po and isinstance(po['beats'], list): events = [] for act in po['beats']: if 'plot_points' in act and isinstance(act['plot_points'], list): for pp in act['plot_points']: desc = pp.get('description') point = pp.get('point', 'Event') if desc: events.append({"description": desc, "purpose": point}) if events: utils.log("ARCHITECT", f"Using {len(events)} events from Plot Outline as base structure.") return events structure_type = bp.get('book_metadata', {}).get('structure_prompt') if not structure_type: label = bp.get('length_settings', {}).get('label', 'Novel') structures = { "Chapter Book": "Create a simple episodic structure with clear chapter hooks.", "Young Adult": "Create a character-driven arc with high emotional stakes and a clear 'Coming of Age' theme.", "Flash Fiction": "Create a single, impactful scene structure with a twist.", "Short Story": "Create a concise narrative arc (Inciting Incident -> Rising Action -> Climax -> Resolution).", "Novella": "Create a standard 3-Act Structure.", "Novel": "Create a detailed 3-Act Structure with A and B plots.", "Epic": "Create a complex, multi-arc structure (Hero's Journey) with extensive world-building events." } structure_type = structures.get(label, "Create a 3-Act Structure.") beats_context = [] if 'plot_outline' in bp and isinstance(bp['plot_outline'], dict): po = bp['plot_outline'] if 'beats' in po: for act in po['beats']: beats_context.append(f"ACT {act.get('act', '?')}: {act.get('title', '')} - {act.get('summary', '')}") for pp in act.get('plot_points', []): beats_context.append(f" * {pp.get('point', 'Beat')}: {pp.get('description', '')}") if not beats_context: beats_context = bp.get('plot_beats', []) prompt = f"{structure_type}\nTITLE: {bp['book_metadata']['title']}\nBEATS: {json.dumps(beats_context)}\nReturn JSON: {{'events': [{{'description':'...', 'purpose':'...'}}]}}" 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))['events'] except: return [] def expand(events, pass_num, target_chapters, bp, folder): utils.log("ARCHITECT", f"Expansion pass {pass_num} | Current Beats: {len(events)} | Target Chaps: {target_chapters}") beats_context = [] if 'plot_outline' in bp and isinstance(bp['plot_outline'], dict): po = bp['plot_outline'] if 'beats' in po: for act in po['beats']: beats_context.append(f"ACT {act.get('act', '?')}: {act.get('title', '')} - {act.get('summary', '')}") for pp in act.get('plot_points', []): beats_context.append(f" * {pp.get('point', 'Beat')}: {pp.get('description', '')}") if not beats_context: beats_context = bp.get('plot_beats', []) prompt = f""" You are a Story Architect. Goal: Flesh out this outline for a {target_chapters}-chapter book. Current Status: {len(events)} beats. ORIGINAL OUTLINE: {json.dumps(beats_context)} INSTRUCTIONS: 1. Look for jumps in time or logic. 2. Insert new intermediate events to smooth the pacing. 3. Deepen subplots while staying true to the ORIGINAL OUTLINE. 4. Do NOT remove or drastically alter the original outline points; expand AROUND them. CURRENT EVENTS: {json.dumps(events)} Return JSON: {{'events': [ ...updated full list... ]}} """ try: response = ai.model_logic.generate_content(prompt) utils.log_usage(folder, "logic-pro", response.usage_metadata) new_events = json.loads(utils.clean_json(response.text))['events'] if len(new_events) > len(events): utils.log("ARCHITECT", f" -> Added {len(new_events) - len(events)} new beats.") elif len(str(new_events)) > len(str(events)) + 20: utils.log("ARCHITECT", f" -> Fleshed out descriptions (Text grew by {len(str(new_events)) - len(str(events))} chars).") else: utils.log("ARCHITECT", " -> No significant changes.") return new_events except Exception as e: utils.log("ARCHITECT", f" -> Pass skipped due to error: {e}") return events def create_chapter_plan(events, bp, folder): utils.log("ARCHITECT", "Finalizing Chapters...") target = bp['length_settings']['chapters'] words = bp['length_settings'].get('words', 'Flexible') include_prologue = bp.get('length_settings', {}).get('include_prologue', False) include_epilogue = bp.get('length_settings', {}).get('include_epilogue', False) structure_instructions = "" if include_prologue: structure_instructions += "- Include a 'Prologue' (chapter_number: 0) to set the scene.\n" if include_epilogue: structure_instructions += "- Include an 'Epilogue' (chapter_number: 'Epilogue') to wrap up.\n" meta = bp.get('book_metadata', {}) style = meta.get('style', {}) pov_chars = style.get('pov_characters', []) pov_instruction = "" if pov_chars: pov_instruction = f"- Assign a 'pov_character' for each chapter from this list: {json.dumps(pov_chars)}." prompt = f""" Group events into Chapters. TARGET CHAPTERS: {target} (Approximate. Feel free to adjust +/- 20% for better pacing). TARGET WORDS: {words} (Total for the book). INSTRUCTIONS: - Vary chapter pacing. Options: 'Very Fast', 'Fast', 'Standard', 'Slow', 'Very Slow'. - Assign an estimated word count to each chapter based on its pacing and content. {structure_instructions} {pov_instruction} EVENTS: {json.dumps(events)} Return JSON: [{{'chapter_number':1, 'title':'...', 'pov_character': 'Name', 'pacing': 'Standard', 'estimated_words': 2000, 'beats':[...]}}] """ try: response = ai.model_logic.generate_content(prompt) utils.log_usage(folder, "logic-pro", response.usage_metadata) plan = json.loads(utils.clean_json(response.text)) target_str = str(words).lower().replace(',', '').replace('k', '000').replace('+', '').replace(' ', '') target_val = 0 if '-' in target_str: try: parts = target_str.split('-') target_val = int((int(parts[0]) + int(parts[1])) / 2) except: pass else: try: target_val = int(target_str) except: pass if target_val > 0: variance = random.uniform(0.90, 1.10) target_val = int(target_val * variance) utils.log("ARCHITECT", f"Target adjusted with variance ({variance:.2f}x): {target_val} words.") current_sum = sum(int(c.get('estimated_words', 0)) for c in plan) if current_sum > 0: factor = target_val / current_sum utils.log("ARCHITECT", f"Adjusting chapter lengths by {factor:.2f}x to match target.") for c in plan: c['estimated_words'] = int(c.get('estimated_words', 0) * factor) return plan except Exception as e: utils.log("ARCHITECT", f"Failed to create chapter plan: {e}") return [] def update_tracking(folder, chapter_num, chapter_text, current_tracking): utils.log("TRACKER", f"Updating world state & character visuals for Ch {chapter_num}...") prompt = f""" Analyze this chapter text to update the Story Bible. CURRENT TRACKING DATA: {json.dumps(current_tracking)} NEW CHAPTER TEXT: {chapter_text[:500000]} TASK: 1. EVENTS: Append 1-3 concise bullet points summarizing key plot events in this chapter to the 'events' list. 2. CHARACTERS: Update entries for any characters appearing in the scene. - "descriptors": List of strings. Add PERMANENT physical traits (height, hair, eyes), specific items (jewelry, weapons). Avoid duplicates. - "likes_dislikes": List of strings. Add specific preferences, likes, or dislikes mentioned (e.g., "Hates coffee", "Loves jazz"). - "last_worn": String. Update if specific clothing is described. IMPORTANT: If a significant time jump occurred (e.g. next day) and no new clothing is described, reset this to "Unknown". - "major_events": List of strings. Log significant life-altering events occurring in THIS chapter (e.g. "Lost an arm", "Married", "Betrayed by X"). 3. CONTENT_WARNINGS: List of strings. Identify specific triggers present in this chapter (e.g. "Graphic Violence", "Sexual Assault", "Torture", "Self-Harm"). Append to existing list. RETURN JSON with the SAME structure as CURRENT TRACKING DATA (events list, characters dict, content_warnings list). """ try: response = ai.model_logic.generate_content(prompt) utils.log_usage(folder, "logic-pro", response.usage_metadata) new_data = json.loads(utils.clean_json(response.text)) return new_data except Exception as e: utils.log("TRACKER", f"Failed to update tracking: {e}") return current_tracking def evaluate_chapter_quality(text, chapter_title, model, folder): prompt = f""" Analyze this book chapter text. CHAPTER TITLE: {chapter_title} CRITERIA: 1. ORGANIC FEEL: Does it sound like a human wrote it? Are "AI-isms" (e.g. 'testament to', 'tapestry', 'shiver down spine', 'unspoken agreement') absent? 2. ENGAGEMENT: Is it interesting? Does it hook the reader? 3. REPETITION: Is sentence structure varied? Are words repeated unnecessarily? 4. PROGRESSION: Does the story move forward, or is it spinning its wheels? Rate on a scale of 1-10. Provide a concise critique focusing on the biggest flaw. Return JSON: {{'score': int, 'critique': 'string'}} """ try: response = model.generate_content([prompt, text[:30000]]) utils.log_usage(folder, "logic-pro", response.usage_metadata) data = json.loads(utils.clean_json(response.text)) return data.get('score', 0), data.get('critique', 'No critique provided.') except Exception as e: return 0, f"Evaluation error: {str(e)}" def create_initial_persona(bp, folder): utils.log("SYSTEM", "Generating initial Author Persona based on genre/tone...") meta = bp.get('book_metadata', {}) style = meta.get('style', {}) prompt = f""" Create a fictional 'Author Persona' best suited to write this book. BOOK DETAILS: Title: {meta.get('title')} Genre: {meta.get('genre')} Tone: {style.get('tone')} Target Audience: {meta.get('target_audience')} TASK: Create a profile for the ideal writer of this book. Return JSON: {{ "name": "Pen Name", "bio": "Description of writing style (voice, sentence structure, vocabulary)...", "age": "...", "gender": "..." }} """ 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("SYSTEM", f"Persona generation failed: {e}") return {"name": "AI Author", "bio": "Standard, balanced writing style."} def refine_persona(bp, text, folder): utils.log("SYSTEM", "Refining Author Persona based on recent chapters...") ad = bp.get('book_metadata', {}).get('author_details', {}) current_bio = ad.get('bio', 'Standard style.') prompt = f""" Analyze this text sample from the book. TEXT: {text[:3000]} CURRENT AUTHOR BIO: {current_bio} TASK: Refine the Author Bio to better match the actual text produced. Highlight specific stylistic quirks, sentence patterns, or vocabulary choices found in the text. The goal is to ensure future chapters sound exactly like this one. Return JSON: {{ "bio": "Updated bio..." }} """ try: response = ai.model_logic.generate_content(prompt) utils.log_usage(folder, "logic-pro", response.usage_metadata) new_bio = json.loads(utils.clean_json(response.text)).get('bio') if new_bio: ad['bio'] = new_bio utils.log("SYSTEM", " -> Persona bio updated.") return ad except: pass return ad def write_chapter(chap, bp, folder, prev_sum, tracking=None, prev_content=None): pacing = chap.get('pacing', 'Standard') est_words = chap.get('estimated_words', 'Flexible') utils.log("WRITER", f"Drafting Ch {chap['chapter_number']} ({pacing} | ~{est_words} words): {chap['title']}") ls = bp['length_settings'] meta = bp.get('book_metadata', {}) style = meta.get('style', {}) pov_char = chap.get('pov_character', '') ad = meta.get('author_details', {}) if not ad and 'author_bio' in meta: persona_info = meta['author_bio'] else: persona_info = f"Name: {ad.get('name', meta.get('author', 'Unknown'))}\n" if ad.get('age'): persona_info += f"Age: {ad['age']}\n" if ad.get('gender'): persona_info += f"Gender: {ad['gender']}\n" if ad.get('race'): persona_info += f"Race: {ad['race']}\n" if ad.get('nationality'): persona_info += f"Nationality: {ad['nationality']}\n" if ad.get('language'): persona_info += f"Language: {ad['language']}\n" if ad.get('bio'): persona_info += f"Style/Bio: {ad['bio']}\n" samples = [] if ad.get('sample_text'): samples.append(f"--- SAMPLE PARAGRAPH ---\n{ad['sample_text']}") if ad.get('sample_files'): for fname in ad['sample_files']: fpath = os.path.join(config.PERSONAS_DIR, fname) if os.path.exists(fpath): try: with open(fpath, 'r', encoding='utf-8', errors='ignore') as f: content = f.read(3000) samples.append(f"--- SAMPLE FROM {fname} ---\n{content}...") except: pass if samples: persona_info += "\nWRITING STYLE SAMPLES:\n" + "\n".join(samples) char_visuals = "" if tracking and 'characters' in tracking: char_visuals = "\nCHARACTER TRACKING (Visuals & Preferences):\n" for name, data in tracking['characters'].items(): desc = ", ".join(data.get('descriptors', [])) likes = ", ".join(data.get('likes_dislikes', [])) worn = data.get('last_worn', 'Unknown') char_visuals += f"- {name}: {desc}\n * Likes/Dislikes: {likes}\n" major = data.get('major_events', []) if major: char_visuals += f" * Major Events: {'; '.join(major)}\n" if worn and worn != 'Unknown': char_visuals += f" * Last Worn: {worn} (NOTE: Only relevant if scene is continuous from previous chapter)\n" style_block = "\n".join([f"- {k.replace('_', ' ').title()}: {v}" for k, v in style.items() if isinstance(v, (str, int, float))]) if 'tropes' in style and isinstance(style['tropes'], list): style_block += f"\n- Tropes: {', '.join(style['tropes'])}" if 'formatting_rules' in style and isinstance(style['formatting_rules'], list): style_block += "\n- Formatting Rules:\n * " + "\n * ".join(style['formatting_rules']) prev_context_block = "" if prev_content: prev_context_block = f"\nPREVIOUS CHAPTER TEXT (For Tone & Continuity):\n{prev_content}\n" prompt = f""" Write Chapter {chap['chapter_number']}: {chap['title']} PACING GUIDE: - Format: {ls.get('label', 'Story')} - Chapter Pacing: {pacing} - Target Word Count: ~{est_words} (Use this as a guide, but prioritize story flow. Allow flexibility.) - POV Character: {pov_char if pov_char else 'Protagonist'} STYLE & FORMATTING: {style_block} AUTHOR VOICE (CRITICAL): {persona_info} INSTRUCTION: Write the scene. - Start with the Chapter Header formatted as Markdown H1 (e.g. '# Chapter X: Title'). Follow the 'Formatting Rules' for the header style. - DEEP POV: Immerse the reader in the POV character's immediate experience. Filter descriptions through their specific worldview and emotional state. - SHOW, DON'T TELL: Focus on immediate action and internal reaction. Don't summarize feelings; show the physical manifestation of them. - SENSORY DETAILS: Use specific, grounding sensory details (smell, touch, sound) rather than generic descriptions. - AVOID CLICHÉS: Avoid common AI tropes (e.g., 'shiver down spine', 'palpable tension', 'unspoken agreement', 'testament to'). - MAINTAIN CONTINUITY: Pay close attention to the PREVIOUS CONTEXT. Characters must NOT know things that haven't happened yet or haven't been revealed to them. - CHARACTER INTERACTIONS: If characters are meeting for the first time in the summary, treat them as strangers. - SENTENCE VARIETY: Avoid repetitive sentence structures (e.g. starting multiple sentences with "He" or "She"). Vary sentence length to create rhythm. - 'Very Fast': Rapid fire, pure action/dialogue, minimal description. - 'Fast': Punchy, keep it moving. - 'Standard': Balanced dialogue and description. - 'Slow': Detailed, atmospheric, immersive. - 'Very Slow': Deep introspection, heavy sensory detail, slow burn. PREVIOUS CONTEXT (Story So Far): {prev_sum} {prev_context_block} CHARACTERS: {json.dumps(bp['characters'])} {char_visuals} SCENE BEATS: {json.dumps(chap['beats'])} Output Markdown. """ current_text = "" try: resp_draft = ai.model_writer.generate_content(prompt) utils.log_usage(folder, "writer-flash", resp_draft.usage_metadata) current_text = resp_draft.text except Exception as e: utils.log("WRITER", f"⚠️ Failed Ch {chap['chapter_number']}: {e}") return f"## Chapter {chap['chapter_number']} Failed\n\nError: {e}" # Refinement Loop max_attempts = 3 best_score = 0 best_text = current_text for attempt in range(1, max_attempts + 1): utils.log("WRITER", f" -> Evaluating Ch {chap['chapter_number']} (Attempt {attempt}/{max_attempts})...") score, critique = evaluate_chapter_quality(current_text, chap['title'], ai.model_logic, folder) if "Evaluation error" in critique: utils.log("WRITER", f" ⚠️ {critique}. Keeping current draft.") if best_score == 0: best_text = current_text break utils.log("WRITER", f" Score: {score}/10. Critique: {critique}") if score >= 8: utils.log("WRITER", " Quality threshold met.") return current_text if score > best_score: best_score = score best_text = current_text if attempt == max_attempts: utils.log("WRITER", " Max attempts reached. Using best version.") return best_text utils.log("WRITER", f" -> Refining Ch {chap['chapter_number']} based on feedback...") refine_prompt = f""" Act as a Senior Editor. Rewrite this chapter to fix the issues identified below. CRITIQUE TO ADDRESS: {critique} ADDITIONAL OBJECTIVES: 1. NATURAL FLOW: Fix stilted phrasing. Ensure the prose flows naturally for the genre ({meta.get('genre', 'Fiction')}) and tone ({style.get('tone', 'Standard')}). 2. HUMANIZATION: Remove robotic phrasing. Ensure dialogue has subtext, interruptions, and distinct voices. Remove "AI-isms" (e.g. 'testament to', 'tapestry of', 'symphony of'). 3. SENTENCE VARIETY: Check for and fix repetitive sentence starts or uniform sentence lengths. The prose should have a dynamic rhythm. 4. CONTINUITY: Ensure consistency with the Story So Far. STORY SO FAR: {prev_sum} {prev_context_block} CURRENT DRAFT: {current_text} Return the polished, final version of the chapter in Markdown. """ try: resp_refine = ai.model_writer.generate_content(refine_prompt) utils.log_usage(folder, "writer-flash", resp_refine.usage_metadata) current_text = resp_refine.text except Exception as e: utils.log("WRITER", f"Refinement failed: {e}") return best_text return best_text def harvest_metadata(bp, folder, full_manuscript): utils.log("HARVESTER", "Scanning for new characters...") full_text = "\n".join([c['content'] for c in full_manuscript])[:50000] prompt = f"Identify new significant characters NOT in:\n{json.dumps(bp['characters'])}\nTEXT:\n{full_text}\nReturn JSON: {{'new_characters': [{{'name':'...', 'role':'...', 'description':'...'}}]}}" try: response = ai.model_logic.generate_content(prompt) utils.log_usage(folder, "logic-pro", response.usage_metadata) new_chars = json.loads(utils.clean_json(response.text)).get('new_characters', []) if new_chars: utils.log("HARVESTER", f"Found {len(new_chars)} new chars.") bp['characters'].extend(new_chars) except: pass return bp def update_persona_sample(bp, folder): utils.log("SYSTEM", "Extracting author persona from manuscript...") ms_path = os.path.join(folder, "manuscript.json") if not os.path.exists(ms_path): return ms = utils.load_json(ms_path) if not ms: return # 1. Extract Text Sample full_text = "\n".join([c.get('content', '') for c in ms]) if len(full_text) < 500: return # 2. Save Sample File if not os.path.exists(config.PERSONAS_DIR): os.makedirs(config.PERSONAS_DIR) meta = bp.get('book_metadata', {}) safe_title = "".join([c for c in meta.get('title', 'book') if c.isalnum() or c=='_']).replace(" ", "_")[:20] timestamp = int(time.time()) filename = f"sample_{safe_title}_{timestamp}.txt" filepath = os.path.join(config.PERSONAS_DIR, filename) sample_text = full_text[:3000] with open(filepath, 'w', encoding='utf-8') as f: f.write(sample_text) # 3. Update or Create Persona author_name = meta.get('author', 'Unknown Author') personas = {} if os.path.exists(config.PERSONAS_FILE): try: with open(config.PERSONAS_FILE, 'r') as f: personas = json.load(f) except: pass if author_name not in personas: utils.log("SYSTEM", f"Generating new persona profile for '{author_name}'...") prompt = f"Analyze this writing style (Tone, Voice, Vocabulary). Write a 1-sentence author bio describing it.\nTEXT: {sample_text[:1000]}" try: response = ai.model_logic.generate_content(prompt) utils.log_usage(folder, "logic-pro", response.usage_metadata) bio = response.text.strip() except: bio = "Style analysis unavailable." personas[author_name] = { "name": author_name, "bio": bio, "sample_files": [filename], "sample_text": sample_text[:500] } else: utils.log("SYSTEM", f"Updating persona '{author_name}' with new sample.") if 'sample_files' not in personas[author_name]: personas[author_name]['sample_files'] = [] if filename not in personas[author_name]['sample_files']: personas[author_name]['sample_files'].append(filename) with open(config.PERSONAS_FILE, 'w') as f: json.dump(personas, f, indent=2) def refine_bible(bible, instruction, folder): utils.log("SYSTEM", f"Refining Bible with instruction: {instruction}") prompt = f""" Act as a Book Editor. CURRENT JSON: {json.dumps(bible)} USER INSTRUCTION: {instruction} TASK: Update the JSON based on the instruction. Maintain valid JSON structure. RETURN ONLY THE JSON. """ try: response = ai.model_logic.generate_content(prompt) utils.log_usage(folder, "logic-pro", response.usage_metadata) new_data = json.loads(utils.clean_json(response.text)) return new_data except Exception as e: utils.log("SYSTEM", f"Refinement failed: {e}") return None