v2.0.0: Modularize project into single-responsibility packages
Replaced monolithic modules/ package with a clean architecture:
- core/ config.py, utils.py
- ai/ models.py (ResilientModel), setup.py (init_models)
- story/ planner.py, writer.py, editor.py, style_persona.py, bible_tracker.py
- marketing/ cover.py, blurb.py, fonts.py, assets.py
- export/ exporter.py
- web/ app.py (Flask factory), db.py, helpers.py, tasks.py, routes/{auth,project,run,persona,admin}.py
- cli/ engine.py (run_generation), wizard.py (BookWizard)
Flask routes split into 5 Blueprints; all templates updated with blueprint-
prefixed url_for() calls. Dockerfile and docker-compose updated to use
web.app entry point and new package paths.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
135
web/routes/persona.py
Normal file
135
web/routes/persona.py
Normal file
@@ -0,0 +1,135 @@
|
||||
import os
|
||||
import json
|
||||
from flask import Blueprint, render_template, request, redirect, url_for, flash
|
||||
from flask_login import login_required
|
||||
from core import config, utils
|
||||
from ai import models as ai_models
|
||||
from ai import setup as ai_setup
|
||||
|
||||
persona_bp = Blueprint('persona', __name__)
|
||||
|
||||
|
||||
@persona_bp.route('/personas')
|
||||
@login_required
|
||||
def list_personas():
|
||||
personas = {}
|
||||
if os.path.exists(config.PERSONAS_FILE):
|
||||
try:
|
||||
with open(config.PERSONAS_FILE, 'r') as f: personas = json.load(f)
|
||||
except: pass
|
||||
return render_template('personas.html', personas=personas)
|
||||
|
||||
|
||||
@persona_bp.route('/persona/new')
|
||||
@login_required
|
||||
def new_persona():
|
||||
return render_template('persona_edit.html', persona={}, name="")
|
||||
|
||||
|
||||
@persona_bp.route('/persona/<string:name>')
|
||||
@login_required
|
||||
def edit_persona(name):
|
||||
personas = {}
|
||||
if os.path.exists(config.PERSONAS_FILE):
|
||||
try:
|
||||
with open(config.PERSONAS_FILE, 'r') as f: personas = json.load(f)
|
||||
except: pass
|
||||
|
||||
persona = personas.get(name)
|
||||
if not persona:
|
||||
flash(f"Persona '{name}' not found.")
|
||||
return redirect(url_for('persona.list_personas'))
|
||||
|
||||
return render_template('persona_edit.html', persona=persona, name=name)
|
||||
|
||||
|
||||
@persona_bp.route('/persona/save', methods=['POST'])
|
||||
@login_required
|
||||
def save_persona():
|
||||
old_name = request.form.get('old_name')
|
||||
name = request.form.get('name')
|
||||
|
||||
if not name:
|
||||
flash("Persona name is required.")
|
||||
return redirect(url_for('persona.list_personas'))
|
||||
|
||||
personas = {}
|
||||
if os.path.exists(config.PERSONAS_FILE):
|
||||
try:
|
||||
with open(config.PERSONAS_FILE, 'r') as f: personas = json.load(f)
|
||||
except: pass
|
||||
|
||||
if old_name and old_name != name and old_name in personas:
|
||||
del personas[old_name]
|
||||
|
||||
persona = {
|
||||
"name": name,
|
||||
"bio": request.form.get('bio'),
|
||||
"age": request.form.get('age'),
|
||||
"gender": request.form.get('gender'),
|
||||
"race": request.form.get('race'),
|
||||
"nationality": request.form.get('nationality'),
|
||||
"language": request.form.get('language'),
|
||||
"sample_text": request.form.get('sample_text'),
|
||||
"voice_keywords": request.form.get('voice_keywords'),
|
||||
"style_inspirations": request.form.get('style_inspirations')
|
||||
}
|
||||
|
||||
personas[name] = persona
|
||||
|
||||
with open(config.PERSONAS_FILE, 'w') as f: json.dump(personas, f, indent=2)
|
||||
|
||||
flash(f"Persona '{name}' saved.")
|
||||
return redirect(url_for('persona.list_personas'))
|
||||
|
||||
|
||||
@persona_bp.route('/persona/delete/<string:name>', methods=['POST'])
|
||||
@login_required
|
||||
def delete_persona(name):
|
||||
personas = {}
|
||||
if os.path.exists(config.PERSONAS_FILE):
|
||||
try:
|
||||
with open(config.PERSONAS_FILE, 'r') as f: personas = json.load(f)
|
||||
except: pass
|
||||
|
||||
if name in personas:
|
||||
del personas[name]
|
||||
with open(config.PERSONAS_FILE, 'w') as f: json.dump(personas, f, indent=2)
|
||||
flash(f"Persona '{name}' deleted.")
|
||||
|
||||
return redirect(url_for('persona.list_personas'))
|
||||
|
||||
|
||||
@persona_bp.route('/persona/analyze', methods=['POST'])
|
||||
@login_required
|
||||
def analyze_persona():
|
||||
try: ai_setup.init_models()
|
||||
except: pass
|
||||
|
||||
if not ai_models.model_logic:
|
||||
return {"error": "AI models not initialized."}, 500
|
||||
|
||||
data = request.json
|
||||
sample = data.get('sample_text', '')
|
||||
|
||||
prompt = f"""
|
||||
ROLE: Literary Analyst
|
||||
TASK: Create or analyze an Author Persona profile.
|
||||
|
||||
INPUT_DATA:
|
||||
- NAME: {data.get('name')}
|
||||
- DEMOGRAPHICS: Age: {data.get('age')} | Gender: {data.get('gender')} | Nationality: {data.get('nationality')}
|
||||
- SAMPLE_TEXT: {sample[:3000]}
|
||||
|
||||
INSTRUCTIONS:
|
||||
1. BIO: Write a 2-3 sentence description of the writing style. If sample is provided, analyze it. If not, invent a style that fits the demographics/name.
|
||||
2. KEYWORDS: Comma-separated list of 3-5 adjectives describing the voice (e.g. Gritty, Whimsical, Sarcastic).
|
||||
3. INSPIRATIONS: Comma-separated list of 1-3 famous authors or genres that this style resembles.
|
||||
|
||||
OUTPUT_FORMAT (JSON): {{ "bio": "String", "voice_keywords": "String", "style_inspirations": "String" }}
|
||||
"""
|
||||
try:
|
||||
response = ai_models.model_logic.generate_content(prompt)
|
||||
return json.loads(utils.clean_json(response.text))
|
||||
except Exception as e:
|
||||
return {"error": str(e)}, 500
|
||||
Reference in New Issue
Block a user