Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions src/web/chat/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -586,8 +586,9 @@ def _build_turn_payload(
"Let only characters who are currently present respond; do not force every participant to speak each turn."
)
if normalized_message_kind == "narration" and mode == "act" and controlled_character_name:
response_lower_bound = min(response_limit_hint, max(1, min(2, len(active_participants))))
response_count_rule = (
f"Return {max(2, min(response_limit_hint, len(active_participants)))}-{response_limit_hint} in-world replies "
f"Return {response_lower_bound}-{response_limit_hint} in-world replies "
f"when multiple cast members are present. Other participants besides {controlled_character_name} must speak; "
"do not return only the controlled character's line."
)
Expand Down Expand Up @@ -1102,7 +1103,7 @@ def _choose_response_limit_hint(*, mode: str, active_count: int, turn_id: str, m
lower = min(upper, 2 if active_count <= 2 else 3)
return rng.randint(lower, upper)
if message_kind == "narration" and mode in {"act", "insert"}:
upper = min(4, max(2, active_count))
upper = min(4, max(1, active_count))
lower = 2 if active_count >= 2 else 1
return rng.randint(lower, upper)
upper = min(3, max(1, active_count))
Expand Down
5 changes: 1 addition & 4 deletions src/web/pipeline/automatic_steps.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,17 +126,14 @@ def process_distill_character(
payload=character_payload,
chunk_count=int(chunk_meta.get("chunk_count", 1) or 1),
)
current_review_fields = read_persona_review_fields(load_profile_source(source_path))

assert_run_not_stopped(manifest_path, current_character=character)
on_distill("materializing_character", {"character": character})
materialized = materialize_profile_source(
source_path,
run_dir / "artifacts" / "characters" / novel_id / safe_filename(character),
)
try:
current_review_fields = read_persona_review_fields(load_persona_bundle(materialized["persona_dir"]))
except Exception:
current_review_fields = read_persona_review_fields(load_profile_source(source_path))
change_summary = summarize_redistill_character_change(
character=character,
previous_fields=previous_review_fields,
Expand Down
11 changes: 11 additions & 0 deletions src/web/static/js/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -3184,6 +3184,17 @@ async function handleSendTurn(messageOverride = "", messageKindOverride = "", op
window.clearTimeout(retryFeedbackTimer);
const errorText = String(error?.message || "").trim() || "这句话暂时没有送达。";
if (sessionSnapshot && !silentOptimistic) {
if (typeof UI_BRIDGE_TOOLS?.syncLegacyUiState === "function") {
UI_BRIDGE_TOOLS.syncLegacyUiState("dialogue-session-restore", {
currentDialogueSessionId,
currentDialogueSession: sessionSnapshot,
});
} else if (typeof publishLegacyUiState === "function") {
publishLegacyUiState("dialogue-session-restore", {
currentDialogueSessionId,
currentDialogueSession: sessionSnapshot,
});
}
const failedSession = {
...sessionSnapshot,
transcript: window.buildFailedSendTranscript(sessionSnapshot, message, messageKind, errorText),
Expand Down
54 changes: 54 additions & 0 deletions tests/test_web_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -6401,6 +6401,60 @@ def test_prepare_turn_act_narration_prompt_prioritizes_other_cast_over_controlle
self.assertEqual(hints[0]["priority"], "normal")
self.assertEqual(hints[1]["priority"], "high")

def test_prepare_turn_act_narration_prompt_handles_single_responder(self):
with tempfile.TemporaryDirectory() as tmp:
service = WebRunService(tmp)
service.save_model_settings(
provider="openai-compatible",
model="deepseek-chat",
base_url="https://example.com/v1",
api_key="sk-test",
)
payload = service.create_run(
novel_name="laoshe.txt",
novel_content_base64=base64.b64encode("祥子娶了虎妞。".encode("utf-8")).decode("ascii"),
characters=["祥子", "虎妞"],
)
for name in ("祥子", "虎妞"):
service.ingest_character_result(
payload["run_id"],
character=name,
content_base64=base64.b64encode(
f"- name: {name}\n- novel_id: laoshe\n- core_identity: 人物\n".encode("utf-8")
).decode("ascii"),
)

manifest = service._require_manifest(payload["run_id"])
session = service.dialogue.create_session(
manifest,
mode="act",
participants=["祥子", "虎妞"],
controlled_character="祥子",
)
raw_session = service.dialogue._read_json(
service.dialogue._session_file(payload["run_id"], session["session_id"])
)
service.dialogue._set_session_scene_progress(
raw_session,
{
"present_participants": ["虎妞"],
"offstage_participants": ["祥子"],
},
)
turn_payload = service.dialogue._build_turn_payload(
manifest,
raw_session,
turn_id="turn-act-narration-single",
message="第二天一早,虎妞催祥子出门办事。",
message_kind="narration",
speaker_override="场景提示",
)
response_rule = turn_payload["instructions"]["response_count_rule"]

self.assertEqual(turn_payload["host_action"]["response_limit_hint"], 1)
self.assertIn("Return 1-1 in-world replies", response_rule)
self.assertNotIn("Return 2-1", response_rule)

def test_reorder_plot_push_responses_moves_controlled_character_before_closing_line(self):
payload = {
"mode": "act",
Expand Down
Loading