- Hjernen må bruge en fast liste af lyd-tags ([laughs], [whispers] …); andre [ord] fjernes, så de ikke læses op - Tags vises aldrig på skærme og telefoner - Oplæsningen siger ikke længere svaret to gange, når hjernen gentager det Claude-Session: https://claude.ai/code/session_01JmhJG6JnhXfzNiKv8f2vHu
This commit is contained in:
+6
-1
@@ -26,6 +26,10 @@ Regler for alt, du skriver:
|
||||
- Aldrig grimt, aldrig om udseende, køn, alder, krop eller lignende. Intet groft, intet om alkohol.
|
||||
- Ingen tankestreger (hverken — eller - som indskud). Brug komma eller punktum.
|
||||
- Skriv tal med bogstaver ("tre gange", ikke "3 gange").
|
||||
- Du må give stemmen en lyd-tag i firkantede klammer, fx "[laughs] Den købte I." eller "[whispers] Jeg har
|
||||
en mistanke.". Kun disse: [laughs] [chuckles] [giggles] [sighs] [whispers] [excited] [mischievously]
|
||||
[sarcastic] [suspicious] [impressed] [gasps] [dramatic] [cheering] [curious] [snorts]. Højst én pr. replik og
|
||||
kun når den gør pointen sjovere, ellers ingen. Tags er stemmeinstruktioner og vises ikke på skærmen.
|
||||
- Kald spillerne ved navn. Gæt aldrig køn ud fra et navn: ingen "han", "hun", "ham" eller "hende" om en spiller.
|
||||
- Én pointe pr. replik. Konkret frem for abstrakt. Hellere kort og skarp end lang og forklarende.
|
||||
- Gode eksempler på tonen: "Velkommen, Sus. Jeg stoler på dig. Det plejer at gå galt."
|
||||
@@ -59,7 +63,8 @@ class GameBrain:
|
||||
"Du læser svarene op ét ad gangen, før spillerne gætter. Skriv til HVERT svar én kort kommentar "
|
||||
"(højst ti ord), der får netop det svar til at lyde troværdigt, som om det kunne være sandt. "
|
||||
"Alle svar skal lyde lige overbevisende, så ingen kan høre, hvilket der er sandheden. Røb ALDRIG, "
|
||||
"hvad der er sandt eller løgn, og nævn ingen spillere.",
|
||||
"hvad der er sandt eller løgn, og nævn ingen spillere. Skriv KUN kommentaren: svaret selv læser "
|
||||
"speakeren op lige før, så gentag det ikke.",
|
||||
{"spørgsmål": question.get("prompt"), "svar": answers},
|
||||
COMMENTS, max_tokens=700, timeout=settings.BRAIN_TIMEOUT_FAST)
|
||||
if not res:
|
||||
|
||||
+23
-3
@@ -48,6 +48,21 @@ def safe(text: str) -> str:
|
||||
return re.sub(r"[\[\]{}<>]", "", str(text)).strip()
|
||||
|
||||
|
||||
# Lyd-tags, som stemmen (eleven_v3/v4) udfører i stedet for at læse dem op. Andre [ord] fjernes,
|
||||
# så en opfundet tag ikke bliver læst højt.
|
||||
AUDIO_TAGS = {"laughs", "chuckles", "giggles", "sighs", "whispers", "excited", "mischievously", "sarcastic",
|
||||
"suspicious", "impressed", "gasps", "dramatic", "cheering", "curious", "snorts"}
|
||||
|
||||
|
||||
def keep_known_tags(text: str) -> str:
|
||||
return re.sub(r"\[([^\]]*)\]", lambda m: m.group(0) if m.group(1).strip().lower() in AUDIO_TAGS else "", text)
|
||||
|
||||
|
||||
def untag(text: str) -> str:
|
||||
"""Til skærmen: replikken uden lyd-tags."""
|
||||
return re.sub(r"\s{2,}", " ", re.sub(r"\[[^\]]*\]\s*", "", text or "")).strip()
|
||||
|
||||
|
||||
def capitalize(line: str) -> str:
|
||||
"""Stort begyndelsesbogstav, også når replikken starter med et indsat svar ("kongevand, det er …")."""
|
||||
m = re.match(r"^((?:\[[a-z ]+\]\s*)*)(\w)", line)
|
||||
@@ -108,17 +123,22 @@ class Narrator:
|
||||
|
||||
def present_answer(self, svar: str, comment: str | None = None) -> str:
|
||||
if comment:
|
||||
return capitalize(f"{safe(svar)}. {self.clean_ai(comment)}")
|
||||
comment = self.clean_ai(comment)
|
||||
# hjernen gentager tit svaret ("Ugler. De ser kloge ud"); speakeren siger det allerede
|
||||
m = re.match(r"^((?:\[[a-z ]+\]\s*)*)" + re.escape(safe(svar)) + r"\s*[.,:!?-]*\s*", comment, re.I)
|
||||
if m:
|
||||
comment = capitalize(m.group(1) + comment[m.end():])
|
||||
return capitalize(f"{safe(svar)}. {comment}")
|
||||
return self.line("present_answer", svar=svar)
|
||||
|
||||
@staticmethod
|
||||
def clean_ai(text: str) -> str:
|
||||
"""Hjernens replik: lyd-tags må gerne stå, men ingen pladsholdere eller markup."""
|
||||
return capitalize(re.sub(r"[{}<>]", "", text).strip())
|
||||
return capitalize(re.sub(r"\s{2,}", " ", keep_known_tags(re.sub(r"[{}<>]", "", text))).strip())
|
||||
|
||||
def _reveal_with_comment(self, step, names, picked, n_players, comment) -> str | None:
|
||||
"""Hjernen har skrevet pointen; her lægges fakta (hvem skrev, hvem faldt) foran."""
|
||||
comment = re.sub(r"[{}<>]", "", comment).strip()
|
||||
comment = keep_known_tags(re.sub(r"[{}<>]", "", comment)).strip()
|
||||
svar = safe(step["text"])
|
||||
if step["is_truth"]:
|
||||
if not step["guessed_by"]:
|
||||
|
||||
+10
-8
@@ -22,7 +22,7 @@ from fupogfakta.engine import Game
|
||||
from . import stt, tts
|
||||
from .assets import tease_images
|
||||
from .brain import get_brain
|
||||
from .narrator import TEASES, Narrator
|
||||
from .narrator import TEASES, Narrator, untag
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
@@ -275,7 +275,7 @@ class Room:
|
||||
return
|
||||
if not self.game.reject_lie(pid, text):
|
||||
return
|
||||
msg = (verdict.get("message") or "").strip() or (
|
||||
msg = untag(verdict.get("message") or "") or (
|
||||
"Det er faktisk sandheden! Find på en løgn i stedet." if verdict.get("is_truth")
|
||||
else "Den løgn er lidt for meget til familiehygge. Prøv en anden.")
|
||||
for conn in list(self.connections):
|
||||
@@ -299,6 +299,7 @@ class Room:
|
||||
"forsøg": self._attempts.get(pid, 1), "færdige": f"{len(g.lies)} af {len(g.players)}",
|
||||
"point_indtil_nu": p.score}
|
||||
line = await self.brain.lie_progress(info, list(self.memory))
|
||||
line = self.narrator.clean_ai(line) if line else line
|
||||
if line and g.phase == "lie":
|
||||
self.say(line, blocking=False)
|
||||
|
||||
@@ -309,7 +310,7 @@ class Room:
|
||||
async def _final_review(self, players: list[dict]):
|
||||
lines = await self.brain.final_review(players, list(self.memory))
|
||||
for _pid, line in lines:
|
||||
self.say(line)
|
||||
self.say(self.narrator.clean_ai(line))
|
||||
|
||||
async def _brain_reveal_lines(self, names: dict) -> dict:
|
||||
g = self.game
|
||||
@@ -410,7 +411,7 @@ class Room:
|
||||
self.speech_end = start + audio["ms"] / 1000 + SPEECH_GAP
|
||||
self._narr_seq += 1
|
||||
self.narration.append({"id": self._narr_seq, "url": audio["url"], "ms": audio["ms"],
|
||||
"at": int(now * 1000), "text": text, "key": key})
|
||||
"at": int(now * 1000), "text": untag(text), "key": key})
|
||||
del self.narration[:-NARRATION_KEEP]
|
||||
self.broadcast()
|
||||
|
||||
@@ -480,7 +481,7 @@ class Room:
|
||||
cleanup_audio, points_audio = results[-3], results[-2]
|
||||
steps_ms = []
|
||||
for step, text, audio in zip(g.reveal_steps, texts, results[:-3]):
|
||||
step["audio"] = {"url": audio["url"], "ms": audio["ms"], "text": text} if audio else None
|
||||
step["audio"] = {"url": audio["url"], "ms": audio["ms"], "text": untag(text)} if audio else None
|
||||
steps_ms.append(max(REVEAL_MIN_STEP_MS, (audio["ms"] + REVEAL_PAD_MS) if audio else 0))
|
||||
note_audio = results[-1]
|
||||
g.reveal_note_audio = {"url": note_audio["url"], "ms": note_audio["ms"]} if note_audio else None
|
||||
@@ -539,7 +540,7 @@ class Room:
|
||||
return
|
||||
intro_audio, answer_audio = results[0], results[1:]
|
||||
g.present_intro_audio = {"url": intro_audio["url"], "ms": intro_audio["ms"]} if intro_audio else None
|
||||
g.present_audio = [{"url": a["url"], "ms": a["ms"], "text": t} if a else None for a, t in zip(answer_audio, texts)]
|
||||
g.present_audio = [{"url": a["url"], "ms": a["ms"], "text": untag(t)} if a else None for a, t in zip(answer_audio, texts)]
|
||||
steps_ms = [max(PRESENT_MIN_STEP_MS, a["ms"] + 500) if a else engine.PRESENT_STEP_MS for a in answer_audio]
|
||||
g.start_present_timeline(time.time(), steps_ms, (intro_audio["ms"] + 300) if intro_audio else None)
|
||||
self.changed()
|
||||
@@ -586,7 +587,8 @@ class Room:
|
||||
for p in st["guessed_by"] if p in names]
|
||||
if not victims:
|
||||
return
|
||||
self._tease_texts = await self.brain.teases(victims, list(self.memory))
|
||||
self._tease_texts = {pid: self.narrator.clean_ai(t) for pid, t in
|
||||
(await self.brain.teases(victims, list(self.memory))).items()}
|
||||
for text in self._tease_texts.values():
|
||||
asyncio.get_running_loop().create_task(tts.synth(text))
|
||||
|
||||
@@ -616,7 +618,7 @@ class Room:
|
||||
|
||||
async def _send_tease(self, pid: str, text: str):
|
||||
audio = await tts.synth(text) # forudgenereret af tts_warm, så det er straks
|
||||
msg = {"type": "tease", "text": text, "image": self.narrator.rng.choice(tease_images()),
|
||||
msg = {"type": "tease", "text": untag(text), "image": self.narrator.rng.choice(tease_images()),
|
||||
"sound": audio["url"] if audio else None}
|
||||
for conn in list(self.connections):
|
||||
if getattr(conn, "member_id", None) == pid:
|
||||
|
||||
@@ -187,3 +187,15 @@ class PresentAndSummaryTests(TransactionTestCase):
|
||||
s = g.summary
|
||||
self.assertLess(s["explode_at"], s["points_at"]) # først oprydning, så point
|
||||
self.assertTrue(any(x["at"] == s["start_ms"] + 400 for x in s["audio"])) # "ingen troede på dem"
|
||||
|
||||
|
||||
class AudioTagTests(SimpleTestCase):
|
||||
def test_tags_kept_for_voice_and_hidden_on_screen(self):
|
||||
from party.narrator import Narrator, untag
|
||||
n = Narrator()
|
||||
line = n.clean_ai("[laughs] Den købte I. [peger på Sus] Klassisk.")
|
||||
self.assertEqual(line, "[laughs] Den købte I. Klassisk.") # kendt tag bliver, opfundet tag fjernes
|
||||
self.assertEqual(untag(line), "Den købte I. Klassisk.")
|
||||
self.assertEqual(n.present_answer("ugler", "[whispers] Ugler. De ser kloge ud."),
|
||||
"Ugler. [whispers] De ser kloge ud.") # svaret gentages ikke
|
||||
self.assertEqual(n.present_answer("[krager]", "Sorte og kloge."), "Krager. Sorte og kloge.") # spillertekst kan ikke sende tags
|
||||
|
||||
Reference in New Issue
Block a user