core/utils.py: - estimate_tokens: improved heuristic 4 chars/token → 3.5 chars/token (more accurate) - truncate_to_tokens: added keep_head=True mode for head+tail truncation (better context retention for story summaries that need both opening and recent content) - load_json: explicit exception handling (json.JSONDecodeError, OSError) with log instead of silent returns; added utf-8 encoding with error replacement - log_image_attempt: replaced bare except with (json.JSONDecodeError, OSError); added utf-8 encoding to output write - log_usage: replaced bare except with AttributeError for token count extraction story/bible_tracker.py: - merge_selected_changes: wrapped all int() key casts (char idx, book num, beat idx) in try/except with meaningful log warning instead of crashing on malformed keys - harvest_metadata: replaced bare except:pass with except Exception as e + log message cli/engine.py: - Persona validation: added warning when all 3 attempts fail and substandard persona is accepted — flags elevated voice-drift risk for the run - Lore index updates: throttled from every chapter to every 3 chapters; lore is stable after the first few chapters (~10% token saving per book) - Mid-gen consistency check: now samples first 2 + last 8 chapters instead of passing full manuscript — caps token cost regardless of book length story/writer.py: - Two-pass polish: added local filter-word density check (no API call); skips the Pro polish if density < 1 per 83 words — saves ~8K tokens on already-clean drafts - Polish prompt: added prev_context_block for continuity — polished chapter now maintains seamless flow from the previous chapter's ending marketing/fonts.py: - Separated requests.exceptions.Timeout with specific log message vs generic failure - Added explicit log message when Roboto fallback also fails (returns None) marketing/blurb.py: - Added word count trim: blurbs > 220 words trimmed to last sentence within 220 words - Changed bare except to except Exception as e with log message - Added utf-8 encoding to file writes; logs final word count Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
68 lines
2.9 KiB
Python
68 lines
2.9 KiB
Python
import os
|
|
import json
|
|
from core import utils
|
|
from ai import models as ai_models
|
|
|
|
|
|
def generate_blurb(bp, folder):
|
|
utils.log("MARKETING", "Generating blurb...")
|
|
meta = bp.get('book_metadata', {})
|
|
|
|
beats = bp.get('plot_beats', [])
|
|
beats_text = "\n".join(f" - {b}" for b in beats[:6]) if beats else " - (no beats provided)"
|
|
|
|
chars = bp.get('characters', [])
|
|
protagonist = next((c for c in chars if 'protagonist' in c.get('role', '').lower()), None)
|
|
protagonist_desc = f"{protagonist['name']} — {protagonist.get('description', '')}" if protagonist else "the protagonist"
|
|
|
|
prompt = f"""
|
|
ROLE: Marketing Copywriter
|
|
TASK: Write a compelling back-cover blurb for a {meta.get('genre', 'fiction')} novel.
|
|
|
|
BOOK DETAILS:
|
|
- TITLE: {meta.get('title')}
|
|
- GENRE: {meta.get('genre')}
|
|
- AUDIENCE: {meta.get('target_audience', 'General')}
|
|
- PROTAGONIST: {protagonist_desc}
|
|
- LOGLINE: {bp.get('manual_instruction', '(none)')}
|
|
- KEY PLOT BEATS:
|
|
{beats_text}
|
|
|
|
BLURB STRUCTURE:
|
|
1. HOOK (1-2 sentences): Open with the protagonist's world and the inciting disruption. Make it urgent.
|
|
2. STAKES (2-3 sentences): Raise the central conflict. What does the protagonist stand to lose?
|
|
3. TENSION (1-2 sentences): Hint at the impossible choice or escalating danger without revealing the resolution.
|
|
4. HOOK CLOSE (1 sentence): End with a tantalising question or statement that demands the reader open the book.
|
|
|
|
RULES:
|
|
- 150-200 words total.
|
|
- DO NOT reveal the ending or resolution.
|
|
- Match the genre's marketing tone ({meta.get('genre', 'fiction')}: e.g. thriller = urgent/terse, romance = emotionally charged, fantasy = epic/wondrous, horror = dread-laden).
|
|
- Use present tense for the blurb voice.
|
|
- No "Blurb:", no title prefix, no labels — marketing copy only.
|
|
"""
|
|
try:
|
|
response = ai_models.model_writer.generate_content(prompt)
|
|
utils.log_usage(folder, ai_models.model_writer.name, response.usage_metadata)
|
|
blurb = response.text.strip()
|
|
|
|
# Trim to 220 words if model overshot the 150-200 word target
|
|
words = blurb.split()
|
|
if len(words) > 220:
|
|
blurb = " ".join(words[:220])
|
|
# End at the last sentence boundary within those 220 words
|
|
for end_ch in ['.', '!', '?']:
|
|
last_sent = blurb.rfind(end_ch)
|
|
if last_sent > len(blurb) // 2:
|
|
blurb = blurb[:last_sent + 1]
|
|
break
|
|
utils.log("MARKETING", f" -> Blurb trimmed to {len(blurb.split())} words.")
|
|
|
|
with open(os.path.join(folder, "blurb.txt"), "w", encoding='utf-8') as f:
|
|
f.write(blurb)
|
|
with open(os.path.join(folder, "back_cover.txt"), "w", encoding='utf-8') as f:
|
|
f.write(blurb)
|
|
utils.log("MARKETING", f" -> Blurb: {len(blurb.split())} words.")
|
|
except Exception as e:
|
|
utils.log("MARKETING", f"Failed to generate blurb: {e}")
|