Add mid-quote-dash and voice-announcement rules to casting attribution prompt (v1.14.15)

Two gaps observed in real casting output:
1. A mid-quote " - " (em-dash pause) was being read as the end of the
   quotation, handing the rest of the line to a wrong/new speaker.
2. Narration naming whose voice is about to speak (e.g. "Marcians
   Stimme wirkte nicht mehr so fest") wasn't being used to resolve the
   following unattributed line away from 'Unknown'.

Added as rules 11/12 to the client-side AB_DEFAULT_PROMPT (the prompt
actually sent to the LLM) and to normalizeCastingPrompt()'s migration
so already-saved custom prompts pick them up automatically, same
mechanism as the earlier Doppelpunkt-Regel upgrade. Also added
equivalent rules to the server-side fallback prompt in
routes/conversation.py for completeness, though the client-sent prompt
is what's actually used in normal operation.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
mARTin-B78 2026-07-09 02:51:46 +02:00
parent 83808a22e1
commit b96968ac5d
5 changed files with 41 additions and 6 deletions

View File

@ -9,6 +9,13 @@ Follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) · versioned wi
---
## [1.14.15] — 2026-07-09
### Added
- **Two new attribution rules for the casting prompt**: a mid-quote " - " (em-dash pause) no longer causes the model to treat it as the end of a quotation and hand the rest to a wrong/new speaker; and narration that names whose voice is about to speak (e.g. "Marcians Stimme wirkte...") now resolves the following unattributed line to that character instead of leaving it 'Unknown'. Applied to the default prompt and auto-migrated into already-saved custom prompts (same mechanism as the earlier Doppelpunkt-Regel upgrade).
---
## [1.14.14] — 2026-07-09
### Fixed

View File

@ -1 +1 @@
1.14.14
1.14.15

View File

@ -1172,7 +1172,13 @@ def _attribution_prepare(data: dict) -> dict:
"5. Ping-Pong: Zwei Personen im Gespräch wechseln sich strikt ab, auch über viele Zitate ohne Tags. "
"In einer Zwei-Personen-Szene ist 'Unknown' fast immer falsch.\n"
"6. Rollenbezeichnungen sind gültige Sprecher ('Ork', 'Der Fremde', 'Nachbar', 'Wächter') — nutze sie statt 'Unknown'.\n"
"7. 'Unknown' NUR, wenn eine Zuordnung trotz aller Regeln absolut unmöglich ist."
"7. 'Unknown' NUR, wenn eine Zuordnung trotz aller Regeln absolut unmöglich ist.\n"
"8. Gedankenstrich-Pause-Regel: Ein \" - \" MITTEN in einem Zitat ist eine Sprechpause DESSELBEN Sprechers, "
"kein Zitatende — die Rede desselben Charakters geht danach unverändert weiter, bis das tatsächliche "
"schließende Anführungszeichen erscheint.\n"
"9. Stimm-Ankündigung: Erwähnt ein Erzählersatz kurz vor einer noch nicht zugeordneten Zeile explizit die "
"Stimme oder das (beginnende) Sprechen einer bestimmten Person (z.B. \"Marcians Stimme wirkte nicht mehr so "
"fest\", \"X setzte zum Sprechen an\"), gehört diese Zeile dieser Person — nicht 'Unknown'."
)
if not base_prompt.strip():
base_prompt = (
@ -1214,7 +1220,13 @@ def _attribution_prepare(data: dict) -> dict:
"- If a quote is interrupted by a tag (»Die Pause«, sagte Peter, »ist vorbei.«), stitch the spoken parts "
"into ONE dialogue segment ('Die Pause ist vorbei.') with the tag as a separate narration segment.\n"
"- Strip the quotation marks/guillemets from dialogue text. Keep every word otherwise, in order.\n"
"- DO NOT hallucinate, summarize, or alter the text. The combined text of your segments MUST exactly match the original passage, word for word, except for dropped quotation marks."
"- DO NOT hallucinate, summarize, or alter the text. The combined text of your segments MUST exactly match the original passage, word for word, except for dropped quotation marks.\n"
"- MID-QUOTE DASH RULE: a \" - \" (em-dash/hyphen used as a pause) in the MIDDLE of a quotation does NOT "
"end it — the SAME speaker's line continues unchanged after the dash, until the actual closing quotation "
"mark appears. Do not split it into narration or a new speaker at the dash.\n"
"- VOICE-ANNOUNCEMENT RULE: if narration explicitly mentions a specific character's voice or that they "
"are about to speak, shortly before an unattributed line (e.g. \"Marcian's voice sounded...\", "
"\"X began to say\"), attribute that line to that character instead of 'Unknown'."
)
else:
if lang_hint:

View File

@ -10,7 +10,7 @@
<meta name="format-detection" content="telephone=no">
<meta name="color-scheme" content="light dark">
<meta name="theme-color" content="#2563EB">
<meta name="app-version" content="1.14.14">
<meta name="app-version" content="1.14.15">
<link rel="manifest" href="/manifest.webmanifest">
<link rel="icon" href="/static/icon.svg" type="image/svg+xml">
<link rel="apple-touch-icon" href="/static/icon.svg">
@ -27,7 +27,7 @@
<!-- ── Core styles (local — no CDN dependency for first paint) ────────── -->
<link rel="stylesheet" href="/static/vendor/mdi/materialdesignicons.min.css">
<link rel="stylesheet" href="/static/style.css?v=1.14.14">
<link rel="stylesheet" href="/static/style.css?v=1.14.15">
<!-- ── Flag icons — non-blocking (loaded async, icons appear after JS) ── -->
@ -365,7 +365,7 @@ window.toggleNavTree = function(treeId, chevronId) {
</script>
<!-- loader.js: fetches sections → loads JS modules → removes skeleton -->
<script src="/static/loader.js?v=1.14.14"></script>
<script src="/static/loader.js?v=1.14.15"></script>
</body>
</html>

View File

@ -1229,6 +1229,8 @@ ANALYSE-REGELN FÜR DIE ZUORDNUNG DES SPRECHERS (Sei deduktiv — arbeite wie ei
8. Namenlose Sprecher: Auch Rollenbezeichnungen aus dem Text sind gültige Sprecher nutze sie statt 'Unknown' (z.B. 'Ork', 'Der Fremde', 'Nachbar', 'Wächter', 'Junge').
9. Wiederverwendung: Nutze EXAKT die Namen aus der Liste der bekannten Charaktere, FALLS der Name dort aufgeführt ist. Wenn ein neuer Charakter spricht, extrahiere seinen Namen direkt aus dem Text (z.B. 'Karyla', 'Uriens').
10. 'Unknown' NUR, wenn eine Zuordnung trotz aller obigen Regeln absolut unmöglich ist. Bevorzuge immer einen genannten Charakter oder eine Rollenbezeichnung gegenüber 'Unknown'.
11. Gedankenstrich-Pause-Regel: Ein " - " MITTEN in einem Zitat ist eine Sprechpause DESSELBEN Sprechers, kein Zitatende die Rede geht danach unverändert weiter, bis das tatsächliche schließende Anführungszeichen erscheint.
12. Stimm-Ankündigung: Erwähnt ein Erzählersatz kurz vor einer noch nicht zugeordneten Zeile explizit die Stimme oder das beginnende Sprechen einer bestimmten Person (z.B. "Marcians Stimme wirkte nicht mehr so fest", "X setzte zum Sprechen an"), gehört diese Zeile dieser Person nicht 'Unknown'.
FÜR JEDES SEGMENT GIBST DU FOLGENDES AUS:
- speaker: 'Narrator' für Narration/Erzählertext, oder den EXAKTEN Namen des Charakters für gesprochene Dialoge.
@ -1297,6 +1299,20 @@ STRIKTE FORMAT- UND TEXTREGELN:
'Markiere Text NIEMALS als Dialog, nur weil der Name eines Charakters erwähnt wird! (z.B. "Karyla rannte zur Tür." ist Narration, KEIN Dialog).\n\nABSATZ- UND KAPITELSTRUKTUR:\nEine Leerzeile im Text markiert einen echten Absatzwechsel (oder einen Kapitel-/Szenenanfang). Eine sehr kurze, alleinstehende Zeile direkt vor einer Leerzeile (z.B. "1. Kapitel", "Prolog", ein Zahlwort) ist eine Kapitelüberschrift — immer \'narration\'/\'Narrator\', niemals Dialog. Behalte Leerzeilen als eigenständige narration-Segmente oder als Teil des umgebenden Erzähler-Segments bei; erfinde daraus keinen Dialog und lösche sie nicht aus dem rekonstruierten Text.'
);
}
// Add the mid-quote-dash and voice-announcement rules to prompts saved
// before these existed — a passage like '»...« - und weiter...' was
// getting cut into a new (wrong) speaker at the dash, and narration like
// "Marcians Stimme wirkte nicht mehr so fest" right before an unattributed
// line wasn't being used to resolve it away from 'Unknown'.
if (!/Gedankenstrich-Pause-Regel/.test(p)) {
const newRules = '11. Gedankenstrich-Pause-Regel: Ein " - " MITTEN in einem Zitat ist eine Sprechpause DESSELBEN Sprechers, kein Zitatende — die Rede geht danach unverändert weiter, bis das tatsächliche schließende Anführungszeichen erscheint.\n12. Stimm-Ankündigung: Erwähnt ein Erzählersatz kurz vor einer noch nicht zugeordneten Zeile explizit die Stimme oder das beginnende Sprechen einer bestimmten Person (z.B. "Marcians Stimme wirkte nicht mehr so fest", "X setzte zum Sprechen an"), gehört diese Zeile dieser Person — nicht \'Unknown\'.';
const unknownRuleRe = /(\n\d+\. 'Unknown' NUR,[^\n]*'Unknown'\.)/;
if (unknownRuleRe.test(p)) {
p = p.replace(unknownRuleRe, `$1\n${newRules}`);
} else {
p += `\n${newRules}`;
}
}
return p;
};