forge: rewrite pass — re-author prose in an author's voice
New Forge::rewrite + PassKind::Rewrite. An author re-authors existing chapter prose entirely in their voice — sentence rhythm, word choice, paragraph shape all become theirs — while canon (names, dates, places, events, order, technical facts) is preserved exactly. Not editing; re-authoring. SystemMode::Replace, max effort. skald rewrite --chapter <uuid> [--author slug] overwrites body_md with the rewritten version. The pre-rewrite prose is stashed in the new chapters.body_md_original column on first rewrite (migration 0008, idempotent) so the original is never lost. body_md_tts is cleared — it was annotated against the old prose and must be regenerated by a fresh prepare-narration. prepare-narration gains --single-voice: skips the character speaker roster so no [voice:X] dialogue tags are inserted, only beat markers. Right for one-voice narration. Migration 0008 also extends generation_runs.kind to allow 'rewrite'.
This commit is contained in:
parent
303b6c73f4
commit
d2442f0a87
5 changed files with 371 additions and 5 deletions
|
|
@ -74,6 +74,10 @@ pub enum PassKind {
|
|||
/// prose; output should be byte-identical except for the
|
||||
/// tag insertions.
|
||||
NarratePrep,
|
||||
/// Re-author existing chapter prose in an author's voice. Canon
|
||||
/// (names, dates, events, places, facts) is preserved exactly;
|
||||
/// the prose itself is rewritten. Not editing — re-authoring.
|
||||
Rewrite,
|
||||
}
|
||||
|
||||
impl PassKind {
|
||||
|
|
@ -84,6 +88,7 @@ impl PassKind {
|
|||
Self::Audit => "audit",
|
||||
Self::Summary => "summary",
|
||||
Self::NarratePrep => "narrate_prep",
|
||||
Self::Rewrite => "rewrite",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -237,6 +242,45 @@ impl Forge {
|
|||
Ok(PassOutput { kind: PassKind::NarratePrep, result: r, duration_ms })
|
||||
}
|
||||
|
||||
/// Re-author existing chapter prose in the author's voice. The
|
||||
/// model receives prose written by another hand and rewrites it
|
||||
/// entirely in its own style — sentence rhythm, word choice,
|
||||
/// paragraph shape all become the author's. Canon is preserved
|
||||
/// exactly: names, dates, events, places, technical facts, and
|
||||
/// the sequence of what happens do not change.
|
||||
///
|
||||
/// Author REQUIRED — a rewrite without an author has no target
|
||||
/// voice. SystemMode::Replace; the model BECOMES the author.
|
||||
/// Max effort: re-authoring is the heaviest prose-craft task.
|
||||
pub async fn rewrite(
|
||||
&self,
|
||||
prose: &str,
|
||||
author: &AuthorWithRevision,
|
||||
) -> anyhow::Result<PassOutput> {
|
||||
let scaffold = author
|
||||
.revision
|
||||
.system_template
|
||||
.as_deref()
|
||||
.unwrap_or(DEFAULT_AUTHOR_SCAFFOLD);
|
||||
let system = scaffold
|
||||
.replace("{{display_name}}", &author.author.display_name)
|
||||
.replace("{{pass_directive}}", REWRITE_DIRECTIVE)
|
||||
.replace("{{soul}}", &author.revision.soul);
|
||||
let user_prompt = rewrite_user_prompt(prose);
|
||||
let body = RunRequest {
|
||||
prompt: user_prompt,
|
||||
model: Some(self.model.clone()),
|
||||
system: Some(system),
|
||||
system_mode: Some(SystemMode::Replace),
|
||||
effort: Some(Effort::Max),
|
||||
timeout_secs: Some(1800),
|
||||
..Default::default()
|
||||
};
|
||||
let r = self.client.run(body).await?;
|
||||
let duration_ms = r.duration_ms;
|
||||
Ok(PassOutput { kind: PassKind::Rewrite, result: r, duration_ms })
|
||||
}
|
||||
|
||||
/// Summarize one chapter to ~250 words. The summary feeds into
|
||||
/// the continuation context for older chapters so the token
|
||||
/// budget stays sane on long series (book 12 doesn't carry book 1
|
||||
|
|
@ -349,6 +393,8 @@ const NARRATE_PREP_DIRECTIVE: &str = "This is a NARRATION-ANNOTATION pass. You r
|
|||
|
||||
const HOUSE_NARRATE_PREP_SYSTEM: &str = "You are a senior audiobook director annotating prose for narration. You insert (a) beat markers — `[breath]`, `[pause:Xs]`, `[scene]` — where a skilled narrator would breathe or pause, (b) speaker voice tags `[voice:<slug>]\"...\"[/voice]` wrapping dialogue based on who is speaking (roster supplied in user prompt; leave unattributed dialogue unwrapped), and (c) occasional humanizing narrator stumbles using em-dash repetition or self-correction (sparingly — maybe 1-3 per chapter, on proper nouns or hard words). Apart from those stumbles you do NOT change a word of the prose. Return the prose verbatim plus beat markers, voice tags, and (rare) stumbles inline. No preamble, no commentary.";
|
||||
|
||||
const REWRITE_DIRECTIVE: &str = "This is a REWRITE pass. The user prompt contains a chapter of prose written by another hand. Re-author it entirely in YOUR voice — every sentence reworked in your style: your sentence rhythm, your word choice, your paragraph shape, your way of landing a beat. This is not editing or polishing. It is re-authoring. The reader should not be able to tell another writer ever touched it.\n\nHARD CONSTRAINTS — canon is non-negotiable:\n- Every character name, every date, every place name stays exactly as written.\n- Every event, and the ORDER events happen in, stays exactly as written.\n- Every technical or historical fact stays exactly as written.\n- Do not add new scenes, characters, or events. Do not cut any scene or beat. Same story, same shape — your telling.\n\nReturn ONLY the rewritten chapter prose. Begin with the chapter heading line (`## Chapter N — title`) exactly as in the source. No preamble, no commentary about the rewrite.";
|
||||
|
||||
// ─── User-prompt builders ───────────────────────────────────────
|
||||
|
||||
fn gen_user_prompt(
|
||||
|
|
@ -395,6 +441,19 @@ pub struct CharacterSpeaker {
|
|||
pub hint: Option<String>,
|
||||
}
|
||||
|
||||
fn rewrite_user_prompt(prose: &str) -> String {
|
||||
let mut out = String::with_capacity(prose.len() + 256);
|
||||
out.push_str("# Chapter to re-author\n\n");
|
||||
out.push_str(prose);
|
||||
out.push_str(
|
||||
"\n\n# Task\n\nRe-author the chapter above entirely in your voice. \
|
||||
Preserve all canon — names, dates, places, events, the order they \
|
||||
happen, every technical fact. Change only the prose. Return only \
|
||||
the rewritten chapter, starting with its `## Chapter N` heading.\n",
|
||||
);
|
||||
out
|
||||
}
|
||||
|
||||
fn narrate_prep_user_prompt(prose: &str, characters: &[CharacterSpeaker]) -> String {
|
||||
let mut out = String::with_capacity(prose.len() + 512);
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue