diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4be8c8d..7af5821 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -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
diff --git a/VERSION b/VERSION
index 569a91f..32f1112 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-1.14.14
+1.14.15
diff --git a/routes/conversation.py b/routes/conversation.py
index 65a93c5..30e0155 100644
--- a/routes/conversation.py
+++ b/routes/conversation.py
@@ -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:
diff --git a/static/index.html b/static/index.html
index 94ee58b..304c778 100644
--- a/static/index.html
+++ b/static/index.html
@@ -10,7 +10,7 @@
-
+
@@ -27,7 +27,7 @@
-
+
@@ -365,7 +365,7 @@ window.toggleNavTree = function(treeId, chevronId) {
-
+