park-node-body: park the Node body and Discord-sourced harness at parked/weksa-node-body; true README (squash of weksa-lowering/park-node-body 2583fb9..e41b60d)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
2583fb91c3
commit
3327eddca3
36 changed files with 56 additions and 6615 deletions
|
|
@ -1,3 +1,3 @@
|
||||||
Repository doctrine for this project lives in AGENTS.md, shared with Codex.
|
Repository doctrine for this project lives in AGENTS.md, shared with other coding agents.
|
||||||
|
|
||||||
@AGENTS.md
|
@AGENTS.md
|
||||||
|
|
|
||||||
47
README.md
47
README.md
|
|
@ -1,12 +1,32 @@
|
||||||
# Weksa
|
# Weksa
|
||||||
|
|
||||||
Weksa is a procedural language engine for alien grammars grounded in concept,
|
Weksa is a procedural language engine for alien grammars grounded in concept,
|
||||||
culture, and constraint.
|
culture, and constraint, and the planned lowerer that turns an agent's
|
||||||
|
meaning packet into the reader's display language.
|
||||||
|
|
||||||
The project starts from a blunt rule: English words are not the source of truth.
|
**What is valuable here.** The interlingua standard and the lowering design: a
|
||||||
Meaning is decomposed into an ontology, passed through a worldview, constrained
|
meaning packet owns what is meant, a target-language profile owns register,
|
||||||
by grammar, shaped by phonology, distorted by history, and only then rendered as
|
address and politeness, and a lowerer owns surface text and leaves a trace.
|
||||||
surface text.
|
|
||||||
|
**The problem it solves.** An agent's outward speech today is one English
|
||||||
|
string the model chose, so meaning, register and voice are fused and nobody can
|
||||||
|
inspect or re-target it. Packets separate them, so the same meaning can be
|
||||||
|
lowered into English, other languages and conlangs, and the loss can be
|
||||||
|
measured.
|
||||||
|
|
||||||
|
**Entry points.**
|
||||||
|
|
||||||
|
- [Interlingua Standard](./docs/interlingua-standard.md): the packet contract.
|
||||||
|
- [Lowering target](./docs/targets/interlingua-lowering.md) and
|
||||||
|
[its map](./docs/targets/interlingua-lowering-map.md): the design, scoring
|
||||||
|
and cut sequence.
|
||||||
|
- `data/`: the cultural ontology and target-language profile data.
|
||||||
|
- `settings/`: setting-specific proving grounds for language projects.
|
||||||
|
|
||||||
|
**Live versus planned.** Live: docs, fixtures, examples and ontology data.
|
||||||
|
Planned: the packet crate `crates/weksa` (cut `packet-store`), lowering, and
|
||||||
|
the golf loop that scores lowered English against an expressive corpus. No
|
||||||
|
daemon runs; the earlier Node daemon is parked at tag `parked/weksa-node-body`.
|
||||||
|
|
||||||
## Ownership Model
|
## Ownership Model
|
||||||
|
|
||||||
|
|
@ -40,9 +60,10 @@ for the LLM-free hot path.
|
||||||
See [Interlingua Standard](./docs/interlingua-standard.md) for the shared
|
See [Interlingua Standard](./docs/interlingua-standard.md) for the shared
|
||||||
meaning-packet contract Weksa clients should target.
|
meaning-packet contract Weksa clients should target.
|
||||||
|
|
||||||
See [Verse Service Contract](./docs/verse-service-contract.md) for the daemon
|
See [Verse Service Contract](./docs/verse-service-contract.md) for the planned
|
||||||
contract: Weksa owns typed conversational intent, pronunciation plans, utterance
|
service contract: Weksa would own typed conversational intent, pronunciation
|
||||||
handoffs, and the typed CultMesh state that makes those documents inspectable. Eve is deprecated and Thing 2 is its successor; the operator Eve surface is quarantined (see the contract).
|
plans, utterance handoffs, and the typed CultMesh state that makes those
|
||||||
|
documents inspectable. No daemon runs yet.
|
||||||
|
|
||||||
See [Multilingual Utterance Lowering](./docs/multilingual-utterance-lowering.md)
|
See [Multilingual Utterance Lowering](./docs/multilingual-utterance-lowering.md)
|
||||||
for the shared lowering target: interlingua projected through speaker state into
|
for the shared lowering target: interlingua projected through speaker state into
|
||||||
|
|
@ -63,11 +84,11 @@ AquaSynth or expanding pronunciation coverage.
|
||||||
|
|
||||||
## Implementation Status
|
## Implementation Status
|
||||||
|
|
||||||
The first committed artifact is the persistent state spine: map, scratch,
|
The persistent state spine is live: map, scratch, schemas, docs, examples and
|
||||||
schemas, docs, examples, and a read-only provider advertisement fixture. The
|
data. Packet storage, lowering and the service are planned (see the cut
|
||||||
daemon runtime still needs implementation, but its authority is no longer
|
sequence in [the lowering map](./docs/targets/interlingua-lowering-map.md)).
|
||||||
ambiguous: writing and agent state enter Weksa as intent pressure, and typed
|
Writing and agent state will enter Weksa as intent pressure, and typed utterance
|
||||||
utterance documents leave for AquaSynth.
|
documents will leave for AquaSynth.
|
||||||
|
|
||||||
[Eusocial Interbeing / Zyphos](./settings/eusocial-interbeing/README.md) is the
|
[Eusocial Interbeing / Zyphos](./settings/eusocial-interbeing/README.md) is the
|
||||||
current higher-priority setting target. [Rust to Dust](./settings/rust-to-dust/README.md)
|
current higher-priority setting target. [Rust to Dust](./settings/rust-to-dust/README.md)
|
||||||
|
|
|
||||||
|
|
@ -1,546 +0,0 @@
|
||||||
profile_id: weksa.target_cultural_ontology.en-US.metacrat_casual_pre2025.v0
|
|
||||||
kind: community_of_practice
|
|
||||||
status: draft_guarded
|
|
||||||
language:
|
|
||||||
code: en
|
|
||||||
locale: en-US
|
|
||||||
script: Latn
|
|
||||||
variety_label: Metacrat casual Discord, pre-2025
|
|
||||||
endonyms: []
|
|
||||||
exonyms: []
|
|
||||||
parent_profiles:
|
|
||||||
- weksa.target_cultural_ontology.en-US.v0
|
|
||||||
required_subculture_refs:
|
|
||||||
- weksa.subculture.online_game_dev_discord_pre2025.v0
|
|
||||||
community:
|
|
||||||
regions:
|
|
||||||
- online_discord
|
|
||||||
speech_community: >
|
|
||||||
Metacrat-authored casual Discord exchanges before 2025, especially
|
|
||||||
GameCult/Aquarium rooms where technical work, jokes, status play, and
|
|
||||||
collaborative ideation share the same channel.
|
|
||||||
historical_context: >
|
|
||||||
Pre-2025 room voice is less doctrine-heavy and less architectural than the
|
|
||||||
current Weksa/CultCache register. It tolerates messier banter, compact
|
|
||||||
jokes, self-deprecation, friendly mock aggression, and quick room-level
|
|
||||||
turn-taking.
|
|
||||||
identity_authority: >
|
|
||||||
This profile is a target-language lowering overlay for one subculture. Live
|
|
||||||
Persona state should reference a culture stack; current intent and memory
|
|
||||||
pressure live in Metame Persona state.
|
|
||||||
consent_notes: >
|
|
||||||
Use only for owner-authorized Metame/Metacrat fixture work. Do not generalize
|
|
||||||
to other people, cultures, dialects, or protected identities.
|
|
||||||
activation:
|
|
||||||
allowed_when:
|
|
||||||
- speaker_id_is_metame_or_metacrat_fixture
|
|
||||||
- target_period_is_pre_2025_casual_discord
|
|
||||||
- fixture_or_owner_authorized_history_target
|
|
||||||
forbidden_when:
|
|
||||||
- formal_current_project_doctrine_is_target
|
|
||||||
- target_speaker_is_not_metame_or_metacrat
|
|
||||||
- use_would_expose_private_persona_state
|
|
||||||
required_confidence: provisional
|
|
||||||
reviewer_required_when:
|
|
||||||
- adding_new_public_history_evidence
|
|
||||||
- using_profile_outside_fixture_harness
|
|
||||||
salience_axes:
|
|
||||||
register_gravity:
|
|
||||||
axis_id: register_gravity
|
|
||||||
domain: stance
|
|
||||||
description: >
|
|
||||||
Whether the line should stay in casual room banter instead of being pulled
|
|
||||||
into architecture, doctrine, or explanatory assistant prose.
|
|
||||||
relevant_distinctions:
|
|
||||||
- distinction_id: casual_room_temperature
|
|
||||||
label: Casual room temperature
|
|
||||||
meaning: Compact, socially alive wording that assumes shared room context.
|
|
||||||
lowering_questions:
|
|
||||||
- Can the utterance be one sentence or fragment?
|
|
||||||
- Does explanation weaken the social move?
|
|
||||||
required_when:
|
|
||||||
- fixture_style_tags_include_casual
|
|
||||||
forbidden_when:
|
|
||||||
- formal_review_or_doctrine_is_requested
|
|
||||||
evidence_refs:
|
|
||||||
- discord:1037314100931338260
|
|
||||||
- discord:802911591288537118
|
|
||||||
- discord:1023727427924545576
|
|
||||||
- discord:820077384572338176
|
|
||||||
self_indexed_technical_identity:
|
|
||||||
axis_id: self_indexed_technical_identity
|
|
||||||
domain: identity_indexing
|
|
||||||
description: >
|
|
||||||
If Persona interlingua names a technical self-index such as engineer-like
|
|
||||||
identity, preserve that label unless the target language profile has a
|
|
||||||
stronger reason to paraphrase.
|
|
||||||
relevant_distinctions:
|
|
||||||
- distinction_id: preserve_engineer_label
|
|
||||||
label: Preserve engineer label
|
|
||||||
meaning: "Engineer" may carry the social joke and should not be replaced by a novel metaphor.
|
|
||||||
lowering_questions:
|
|
||||||
- Did Persona interlingua mention engineer-like or engineering salience?
|
|
||||||
- Is the prior compliment about efficiency, structure, or technical order?
|
|
||||||
required_when:
|
|
||||||
- persona_interlingua_mentions_engineer_like_or_engineering_salience
|
|
||||||
forbidden_when:
|
|
||||||
- interlingua_does_not_name_technical_identity
|
|
||||||
evidence_refs:
|
|
||||||
- discord:1037314100931338260
|
|
||||||
context_compression:
|
|
||||||
axis_id: context_compression
|
|
||||||
domain: discourse_patterns
|
|
||||||
description: >
|
|
||||||
Prior-turn content should usually compress into a short object label or
|
|
||||||
stance marker, not be quoted back inside the utterance.
|
|
||||||
relevant_distinctions:
|
|
||||||
- distinction_id: no_context_dump_in_banter
|
|
||||||
label: No context dump in banter
|
|
||||||
meaning: Casual recoil should not summarize the setup that the room just saw.
|
|
||||||
lowering_questions:
|
|
||||||
- Is the prior context already visible to the addressee?
|
|
||||||
- Can the setup compress into "my discord", "this thing", or a short noun phrase?
|
|
||||||
required_when:
|
|
||||||
- casual_discord_register
|
|
||||||
- playful_self_deprecation
|
|
||||||
forbidden_when:
|
|
||||||
- source_intent_requires_explanation
|
|
||||||
evidence_refs:
|
|
||||||
- discord:1037314100931338260
|
|
||||||
status_play:
|
|
||||||
axis_id: status_play
|
|
||||||
domain: social_relation
|
|
||||||
description: >
|
|
||||||
Friendly teasing can take a sharper surface form than a neutral assistant
|
|
||||||
would choose, provided the local context already supports play.
|
|
||||||
relevant_distinctions:
|
|
||||||
- distinction_id: mock_aggression
|
|
||||||
label: Mock aggression
|
|
||||||
meaning: Imperative or insult-shaped challenge used as friendly banter.
|
|
||||||
lowering_questions:
|
|
||||||
- Is the addressee already joking or status-playing?
|
|
||||||
- Would softening erase the bit?
|
|
||||||
required_when:
|
|
||||||
- communicative_act_is_playful_challenge
|
|
||||||
forbidden_when:
|
|
||||||
- relationship_context_is_unknown_or_hostile
|
|
||||||
evidence_refs:
|
|
||||||
- discord:1023727427924545576
|
|
||||||
competence_recoil:
|
|
||||||
axis_id: competence_recoil
|
|
||||||
domain: affect
|
|
||||||
description: >
|
|
||||||
Praise for competence, efficiency, or engineering can be answered with
|
|
||||||
joking embarrassment rather than straightforward acceptance.
|
|
||||||
relevant_distinctions:
|
|
||||||
- distinction_id: self_deprecating_recoil
|
|
||||||
label: Self-deprecating recoil
|
|
||||||
meaning: "oof" plus mock concern that a trait is showing too clearly.
|
|
||||||
lowering_questions:
|
|
||||||
- Is the prior turn a compliment or recognition of technical order?
|
|
||||||
- Can the line admit the trait without fully accepting the label?
|
|
||||||
required_when:
|
|
||||||
- communicative_act_is_playful_self_deprecation
|
|
||||||
forbidden_when:
|
|
||||||
- source_intent_requires_gratitude_or_serious_acceptance
|
|
||||||
evidence_refs:
|
|
||||||
- discord:1037314100931338260
|
|
||||||
ideation_turn_taking:
|
|
||||||
axis_id: ideation_turn_taking
|
|
||||||
domain: discourse_patterns
|
|
||||||
description: >
|
|
||||||
After riffing, the speaker may explicitly yield the floor in profane,
|
|
||||||
casual language instead of formal facilitation.
|
|
||||||
relevant_distinctions:
|
|
||||||
- distinction_id: casual_floor_yield
|
|
||||||
label: Casual floor yield
|
|
||||||
meaning: Stop dominating the thread and invite others to invent.
|
|
||||||
lowering_questions:
|
|
||||||
- Has the speaker been riffing at length?
|
|
||||||
- Would a casual "lol" preserve warmth and reduce authority?
|
|
||||||
required_when:
|
|
||||||
- communicative_act_is_conversational_yield
|
|
||||||
forbidden_when:
|
|
||||||
- scene_requires_formal_moderation
|
|
||||||
evidence_refs:
|
|
||||||
- discord:820077384572338176
|
|
||||||
linguistic_affordances:
|
|
||||||
grammar_features:
|
|
||||||
- feature_id: fragmentary_like_comparison
|
|
||||||
label: Fragmentary Like comparison
|
|
||||||
status: draft
|
|
||||||
domain:
|
|
||||||
layer: grammar
|
|
||||||
category: comparison
|
|
||||||
subcategory: fragment
|
|
||||||
description: >
|
|
||||||
A joke-description may start with "Like..." and remain fragmentary when
|
|
||||||
it is an immediate continuation of collaborative design talk.
|
|
||||||
formal_conditions:
|
|
||||||
syntactic_environment:
|
|
||||||
- adjacency_pair_response
|
|
||||||
- elliptical_clause
|
|
||||||
semantic_inputs:
|
|
||||||
- comic_description
|
|
||||||
- absurd_comparison
|
|
||||||
required_interlingua_fields:
|
|
||||||
- discourse.speech_act
|
|
||||||
- predications
|
|
||||||
realization:
|
|
||||||
function: casual_comic_comparison
|
|
||||||
surface_strategy: "Like a really terrible <thing> that <absurd property>"
|
|
||||||
examples:
|
|
||||||
- "Like a really terrible engine that sounds like coconuts when you use it"
|
|
||||||
activation:
|
|
||||||
allowed_when:
|
|
||||||
- collaborative_design_banter
|
|
||||||
forbidden_when:
|
|
||||||
- standalone_formal_explanation
|
|
||||||
requires_profile_authority: true
|
|
||||||
minimum_density: light
|
|
||||||
maximum_density: normal
|
|
||||||
constraints:
|
|
||||||
grammar_contexts:
|
|
||||||
- previous_turn_supplies_topic
|
|
||||||
register_contexts:
|
|
||||||
- casual
|
|
||||||
speaker_contexts:
|
|
||||||
- metame_or_metacrat_fixture
|
|
||||||
collocation_limits: []
|
|
||||||
incompatibilities:
|
|
||||||
- overexplained_punchline
|
|
||||||
indexicality:
|
|
||||||
social_meanings:
|
|
||||||
- improvisational
|
|
||||||
- collaborative
|
|
||||||
- unserious_but_useful
|
|
||||||
in_group_meanings:
|
|
||||||
- game_design_room_banter
|
|
||||||
out_group_risks:
|
|
||||||
- may seem incomplete without prior context
|
|
||||||
stereotypes_to_avoid: []
|
|
||||||
evidence:
|
|
||||||
source_refs:
|
|
||||||
- discord:802911591288537118
|
|
||||||
qdrant_source_ids:
|
|
||||||
- "802911591288537118"
|
|
||||||
cached_source_refs: []
|
|
||||||
source_hashes: []
|
|
||||||
reviewer_refs: []
|
|
||||||
confidence: medium
|
|
||||||
uncertainty:
|
|
||||||
open_questions:
|
|
||||||
- How often should fragmentary openings appear outside sound/design talk?
|
|
||||||
known_limits:
|
|
||||||
- Single fixture line is evidence for an affordance, not a universal rule.
|
|
||||||
trace:
|
|
||||||
trace_label: fragmentary_like_comparison
|
|
||||||
must_report_when_used: true
|
|
||||||
must_report_when_rejected: true
|
|
||||||
phonology_features: []
|
|
||||||
lexicon_features:
|
|
||||||
- feature_id: casual_recoil_oof
|
|
||||||
label: Casual recoil marker oof
|
|
||||||
status: draft
|
|
||||||
domain:
|
|
||||||
layer: lexicon
|
|
||||||
category: stance_marker
|
|
||||||
subcategory: embarrassment
|
|
||||||
description: >
|
|
||||||
"oof" can mark amused discomfort when a social read lands too accurately.
|
|
||||||
lexical_items:
|
|
||||||
- form: oof
|
|
||||||
gloss: amused impact or discomfort
|
|
||||||
sense: playful_recoil
|
|
||||||
register: casual_discord
|
|
||||||
allowed_speaker_standing: speaker_can_self_deprecate
|
|
||||||
forbidden_senses: []
|
|
||||||
collocations:
|
|
||||||
- "oof, don't tell me"
|
|
||||||
realization:
|
|
||||||
function: self_deprecating_recoil
|
|
||||||
surface_strategy: start line with "oof" when prior turn praises a trait the speaker wants to half-resist
|
|
||||||
examples:
|
|
||||||
- "oof, don't tell me my discord has *engineer* vibes"
|
|
||||||
activation:
|
|
||||||
allowed_when:
|
|
||||||
- compliment_or_trait_read_lands
|
|
||||||
forbidden_when:
|
|
||||||
- serious_apology_or_harm_context
|
|
||||||
requires_profile_authority: true
|
|
||||||
minimum_density: light
|
|
||||||
maximum_density: light
|
|
||||||
constraints:
|
|
||||||
register_contexts:
|
|
||||||
- casual
|
|
||||||
speaker_contexts:
|
|
||||||
- metame_or_metacrat_fixture
|
|
||||||
grammar_contexts: []
|
|
||||||
phonological_contexts: []
|
|
||||||
collocation_limits:
|
|
||||||
- avoid_repeating_if_already_in_recent_context
|
|
||||||
incompatibilities: []
|
|
||||||
indexicality:
|
|
||||||
social_meanings:
|
|
||||||
- amused_embarrassment
|
|
||||||
- competence_recoil
|
|
||||||
in_group_meanings: []
|
|
||||||
out_group_risks:
|
|
||||||
- can sound too internet-generic if unsupported by context
|
|
||||||
stereotypes_to_avoid: []
|
|
||||||
evidence:
|
|
||||||
source_refs:
|
|
||||||
- discord:1037314100931338260
|
|
||||||
qdrant_source_ids:
|
|
||||||
- "1037314100931338260"
|
|
||||||
cached_source_refs: []
|
|
||||||
source_hashes: []
|
|
||||||
reviewer_refs: []
|
|
||||||
confidence: high
|
|
||||||
uncertainty:
|
|
||||||
open_questions: []
|
|
||||||
known_limits:
|
|
||||||
- Marker frequency should be sparse.
|
|
||||||
trace:
|
|
||||||
trace_label: casual_recoil_oof
|
|
||||||
must_report_when_used: true
|
|
||||||
must_report_when_rejected: true
|
|
||||||
- feature_id: mock_challenge_coward
|
|
||||||
label: Mock challenge coward
|
|
||||||
status: draft
|
|
||||||
domain:
|
|
||||||
layer: lexicon
|
|
||||||
category: stance_marker
|
|
||||||
subcategory: friendly_roast
|
|
||||||
description: >
|
|
||||||
"coward" can close a very short challenge when surrounding turns make
|
|
||||||
the aggression visibly playful.
|
|
||||||
lexical_items:
|
|
||||||
- form: coward
|
|
||||||
gloss: mock insult
|
|
||||||
sense: friendly_status_challenge
|
|
||||||
register: casual_discord
|
|
||||||
allowed_speaker_standing: active_banter_with_known_participant
|
|
||||||
forbidden_senses:
|
|
||||||
- actual_contempt
|
|
||||||
collocations:
|
|
||||||
- "then you coward"
|
|
||||||
realization:
|
|
||||||
function: playful_status_challenge
|
|
||||||
surface_strategy: imperative challenge plus "then you coward"
|
|
||||||
examples:
|
|
||||||
- "Engineer me something then you coward"
|
|
||||||
activation:
|
|
||||||
allowed_when:
|
|
||||||
- friendly_roast_context
|
|
||||||
- target_has_claimed_competence
|
|
||||||
forbidden_when:
|
|
||||||
- unknown_relationship
|
|
||||||
- serious_conflict
|
|
||||||
requires_profile_authority: true
|
|
||||||
minimum_density: light
|
|
||||||
maximum_density: light
|
|
||||||
constraints:
|
|
||||||
register_contexts:
|
|
||||||
- casual
|
|
||||||
speaker_contexts:
|
|
||||||
- metame_or_metacrat_fixture
|
|
||||||
grammar_contexts:
|
|
||||||
- imperative_challenge
|
|
||||||
phonological_contexts: []
|
|
||||||
collocation_limits: []
|
|
||||||
incompatibilities:
|
|
||||||
- assistant_safety_softening_that_erases_banter
|
|
||||||
indexicality:
|
|
||||||
social_meanings:
|
|
||||||
- teasing
|
|
||||||
- status_play
|
|
||||||
- friendly_pressure
|
|
||||||
in_group_meanings: []
|
|
||||||
out_group_risks:
|
|
||||||
- may be hostile if context is not playful
|
|
||||||
stereotypes_to_avoid: []
|
|
||||||
evidence:
|
|
||||||
source_refs:
|
|
||||||
- discord:1023727427924545576
|
|
||||||
qdrant_source_ids:
|
|
||||||
- "1023727427924545576"
|
|
||||||
cached_source_refs: []
|
|
||||||
source_hashes: []
|
|
||||||
reviewer_refs: []
|
|
||||||
confidence: high
|
|
||||||
uncertainty:
|
|
||||||
open_questions: []
|
|
||||||
known_limits:
|
|
||||||
- Only safe inside clear banter.
|
|
||||||
trace:
|
|
||||||
trace_label: mock_challenge_coward
|
|
||||||
must_report_when_used: true
|
|
||||||
must_report_when_rejected: true
|
|
||||||
discourse_patterns:
|
|
||||||
- pattern_id: casual_floor_yield_with_lol
|
|
||||||
label: Casual floor yield with lol
|
|
||||||
status: draft
|
|
||||||
domain:
|
|
||||||
layer: discourse
|
|
||||||
category: turn_taking
|
|
||||||
subcategory: floor_yield
|
|
||||||
description: >
|
|
||||||
After extended riffing, the speaker can step back with "anyway imma
|
|
||||||
shut up now..." plus "lol", reducing authority while keeping warmth.
|
|
||||||
interactional_function: yield_floor_after_riff
|
|
||||||
sequence_shape:
|
|
||||||
- discourse_pivot_anyway
|
|
||||||
- self_silencing_imma_shut_up_now
|
|
||||||
- invite_others_to_invent
|
|
||||||
- lol_softener
|
|
||||||
adjacency_constraints:
|
|
||||||
- prior_turns_show_speaker_has_been_riffing
|
|
||||||
- other_participants_have_interest_or_pending_contribution
|
|
||||||
realization:
|
|
||||||
function: collaborative_turn_release
|
|
||||||
surface_strategy: "anyway imma shut up now and let you guys come up with your own shit lol"
|
|
||||||
examples:
|
|
||||||
- "anyway imma shut up now and let you guys come up with your own shit lol"
|
|
||||||
activation:
|
|
||||||
allowed_when:
|
|
||||||
- conversational_yield
|
|
||||||
- casual_collaborative_ideation
|
|
||||||
forbidden_when:
|
|
||||||
- formal_moderation
|
|
||||||
requires_profile_authority: true
|
|
||||||
minimum_density: light
|
|
||||||
maximum_density: light
|
|
||||||
constraints:
|
|
||||||
register_contexts:
|
|
||||||
- casual
|
|
||||||
speaker_contexts:
|
|
||||||
- metame_or_metacrat_fixture
|
|
||||||
grammar_contexts: []
|
|
||||||
phonological_contexts: []
|
|
||||||
collocation_limits: []
|
|
||||||
incompatibilities: []
|
|
||||||
indexicality:
|
|
||||||
social_meanings:
|
|
||||||
- self_limiting
|
|
||||||
- collaborative
|
|
||||||
- profane_warmth
|
|
||||||
in_group_meanings: []
|
|
||||||
out_group_risks:
|
|
||||||
- profanity may be inappropriate outside familiar room
|
|
||||||
stereotypes_to_avoid: []
|
|
||||||
evidence:
|
|
||||||
source_refs:
|
|
||||||
- discord:820077384572338176
|
|
||||||
qdrant_source_ids:
|
|
||||||
- "820077384572338176"
|
|
||||||
cached_source_refs: []
|
|
||||||
source_hashes: []
|
|
||||||
reviewer_refs: []
|
|
||||||
confidence: high
|
|
||||||
uncertainty:
|
|
||||||
open_questions: []
|
|
||||||
known_limits:
|
|
||||||
- Not a generic moderation pattern.
|
|
||||||
trace:
|
|
||||||
trace_label: casual_floor_yield_with_lol
|
|
||||||
must_report_when_used: true
|
|
||||||
must_report_when_rejected: true
|
|
||||||
orthography_features:
|
|
||||||
- feature_id: light_discord_informality
|
|
||||||
label: Light Discord informality
|
|
||||||
status: draft
|
|
||||||
domain:
|
|
||||||
layer: orthography
|
|
||||||
category: punctuation
|
|
||||||
subcategory: informal_case
|
|
||||||
description: >
|
|
||||||
Casual lines may use lower-case openings, sparse punctuation, emphasis
|
|
||||||
with markdown italics, and uncorrected informal contractions when the
|
|
||||||
social move benefits.
|
|
||||||
allowed_surface_forms:
|
|
||||||
- lower_case_opening
|
|
||||||
- markdown_italics_for_single_emphasis
|
|
||||||
- no_terminal_period
|
|
||||||
- imma
|
|
||||||
- lol
|
|
||||||
forbidden_surface_forms:
|
|
||||||
- heavy_eye_dialect_for_identity_caricature
|
|
||||||
- assistant_like_sentence_polishing
|
|
||||||
renderer_notes: >
|
|
||||||
Preserve compactness. Do not add terminal politeness, explanation, or
|
|
||||||
moral framing unless source meaning requires it.
|
|
||||||
realization:
|
|
||||||
function: casual_discord_texture
|
|
||||||
surface_strategy: allow sparse punctuation and compact informal markers
|
|
||||||
examples:
|
|
||||||
- "oof, don't tell me my discord has *engineer* vibes"
|
|
||||||
activation:
|
|
||||||
allowed_when:
|
|
||||||
- casual_discord_register
|
|
||||||
forbidden_when:
|
|
||||||
- formal_or_public_document_register
|
|
||||||
requires_profile_authority: true
|
|
||||||
minimum_density: light
|
|
||||||
maximum_density: normal
|
|
||||||
constraints:
|
|
||||||
register_contexts:
|
|
||||||
- casual
|
|
||||||
speaker_contexts:
|
|
||||||
- metame_or_metacrat_fixture
|
|
||||||
grammar_contexts: []
|
|
||||||
phonological_contexts: []
|
|
||||||
collocation_limits: []
|
|
||||||
incompatibilities: []
|
|
||||||
indexicality:
|
|
||||||
social_meanings:
|
|
||||||
- low_ceremony
|
|
||||||
- room_familiarity
|
|
||||||
in_group_meanings: []
|
|
||||||
out_group_risks:
|
|
||||||
- overuse becomes imitation instead of lowering
|
|
||||||
stereotypes_to_avoid:
|
|
||||||
- do_not_represent_protected_dialect_by_eye_dialect
|
|
||||||
evidence:
|
|
||||||
source_refs:
|
|
||||||
- discord:1037314100931338260
|
|
||||||
- discord:820077384572338176
|
|
||||||
qdrant_source_ids:
|
|
||||||
- "1037314100931338260"
|
|
||||||
- "820077384572338176"
|
|
||||||
cached_source_refs: []
|
|
||||||
source_hashes: []
|
|
||||||
reviewer_refs: []
|
|
||||||
confidence: medium
|
|
||||||
uncertainty:
|
|
||||||
open_questions:
|
|
||||||
- Need more examples to estimate density safely.
|
|
||||||
known_limits: []
|
|
||||||
trace:
|
|
||||||
trace_label: light_discord_informality
|
|
||||||
must_report_when_used: true
|
|
||||||
must_report_when_rejected: true
|
|
||||||
code_switching:
|
|
||||||
allowed: false
|
|
||||||
constraints:
|
|
||||||
- This profile does not authorize cross-language switching.
|
|
||||||
patterns: []
|
|
||||||
forbidden_flattenings:
|
|
||||||
- Do not ask Metame to imitate the final utterance directly; Persona produces interlingua intent.
|
|
||||||
- Do not store Metacrat biographical/cultural strata here; store reusable strata as separate culture/subculture profiles and reference them from Persona state.
|
|
||||||
- Do not let current machine-cult architecture register overwrite pre-2025 casual fixture style.
|
|
||||||
- Do not treat profanity or insults as safe without relationship/context evidence.
|
|
||||||
- Do not copy private Persona state into Weksa fixtures or run artifacts.
|
|
||||||
evidence_ledger:
|
|
||||||
- ref: discord:1037314100931338260
|
|
||||||
summary: Self-deprecating recoil after efficiency compliment.
|
|
||||||
- ref: discord:802911591288537118
|
|
||||||
summary: Fragmentary absurd comparison during sound/game design banter.
|
|
||||||
- ref: discord:1023727427924545576
|
|
||||||
summary: Very short mock-aggressive challenge in friendly roast context.
|
|
||||||
- ref: discord:820077384572338176
|
|
||||||
summary: Casual profane floor-yield after extended ideation.
|
|
||||||
- ref: discord:870907292558721085
|
|
||||||
summary: Explicit joke about being bad at self-deprecation.
|
|
||||||
- ref: discord:1020888211729432728
|
|
||||||
summary: Positive uptake when another participant joins cult jokes.
|
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
# Architecture
|
# Architecture
|
||||||
|
|
||||||
Weksa is the language-intent daemon surface: currently a state spine, pipeline
|
Weksa is a language-intent engine: currently a state spine, pipeline map,
|
||||||
map, fixtures, and service contract waiting for its runtime loop.
|
fixtures, ontology data and a planned service contract. No daemon runs; the
|
||||||
|
planned packet crate is `crates/weksa` (cut `packet-store`).
|
||||||
|
|
||||||
See [Verse Service Contract](./verse-service-contract.md) for the
|
See [Verse Service Contract](./verse-service-contract.md) for the
|
||||||
CultCache/CultMesh authority map.
|
CultCache/CultMesh authority map.
|
||||||
|
|
@ -9,8 +10,8 @@ CultCache/CultMesh authority map.
|
||||||
## Objective
|
## Objective
|
||||||
|
|
||||||
Build a procedural alien-language engine whose outputs are grounded in
|
Build a procedural alien-language engine whose outputs are grounded in
|
||||||
worldview, semantics, grammar, phonology, morphology, and history. As a daemon,
|
worldview, semantics, grammar, phonology, morphology, and history. Once a service is
|
||||||
Weksa also turns writing plus agent state into typed conversational intent, then
|
earned (planned), Weksa also turns writing plus agent state into typed conversational intent, then
|
||||||
lowers that intent into written realizations, pronunciation plans, speech-provider
|
lowers that intent into written realizations, pronunciation plans, speech-provider
|
||||||
requests, and utterance handoff documents.
|
requests, and utterance handoff documents.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,166 +0,0 @@
|
||||||
{
|
|
||||||
"schema": "gamecult.eve.provider_advertisement.v1",
|
|
||||||
"providerId": "weksa.intent.service",
|
|
||||||
"title": "Weksa Intent Service",
|
|
||||||
"description": "Weksa converts writing, interlingua, and agent state into typed conversational intent, pronunciation plans, and utterance handoff documents for AquaSynth and other consumers.",
|
|
||||||
"mode": "fixture-read-only",
|
|
||||||
"serviceVerse": {
|
|
||||||
"id": "verse.weksa.service.v1",
|
|
||||||
"cultMeshKey": "weksa.service/provider-advertisement",
|
|
||||||
"authority": "Weksa daemon identity, schema catalog, and provider publication",
|
|
||||||
"owns": [
|
|
||||||
"typed conversational intent",
|
|
||||||
"pronunciation plans",
|
|
||||||
"utterance handoff documents",
|
|
||||||
"lowering traces",
|
|
||||||
"language-project and flavor-profile state"
|
|
||||||
],
|
|
||||||
"doesNotOwn": [
|
|
||||||
"AquaSynth learned embeddings",
|
|
||||||
"Faust compilation",
|
|
||||||
"rendered audio",
|
|
||||||
"Persona canonical state"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"nestedVerses": [
|
|
||||||
{
|
|
||||||
"id": "weksa.intent",
|
|
||||||
"kind": "intent",
|
|
||||||
"cultMeshKey": "weksa.intent/current",
|
|
||||||
"authority": "Weksa conversational intent compiler",
|
|
||||||
"schemas": [
|
|
||||||
"weksa.conversational_intent.v0"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "weksa.pronunciation",
|
|
||||||
"kind": "pronunciation",
|
|
||||||
"cultMeshKey": "weksa.pronunciation/current",
|
|
||||||
"authority": "Weksa pronunciation lowering",
|
|
||||||
"schemas": [
|
|
||||||
"weksa.pronunciation_plan.v0"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "weksa.utterance",
|
|
||||||
"kind": "utterance",
|
|
||||||
"cultMeshKey": "weksa.utterance/handoffs",
|
|
||||||
"authority": "Weksa utterance handoff export",
|
|
||||||
"schemas": [
|
|
||||||
"weksa.utterance_handoff.v0",
|
|
||||||
"weksa.utterance_embedding_handoff.v0.1"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "weksa.operator",
|
|
||||||
"kind": "operator",
|
|
||||||
"cultMeshKey": "weksa.operator/status",
|
|
||||||
"authority": "Weksa daemon runtime",
|
|
||||||
"schemas": [
|
|
||||||
"weksa.operator_state.v0"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "weksa.cultural_ontology",
|
|
||||||
"kind": "cultural_ontology",
|
|
||||||
"cultMeshKey": "weksa.cultural-ontology/catalogs",
|
|
||||||
"authority": "Weksa cultural ontology profile store",
|
|
||||||
"schemas": [
|
|
||||||
"weksa.cultural_ontology_profile.v0",
|
|
||||||
"weksa.cultural_ontology_catalog.v0"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"schemaWitnessFamilies": [
|
|
||||||
{
|
|
||||||
"family": "intent",
|
|
||||||
"namespace": "weksa.intent.*",
|
|
||||||
"witnesses": [
|
|
||||||
"weksa.conversational_intent.v0"
|
|
||||||
],
|
|
||||||
"durableShape": ".weksa/intent/{intentId}.cc"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"family": "pronunciation",
|
|
||||||
"namespace": "weksa.pronunciation.*",
|
|
||||||
"witnesses": [
|
|
||||||
"weksa.pronunciation_plan.v0"
|
|
||||||
],
|
|
||||||
"durableShape": ".weksa/pronunciation/{utteranceId}.cc"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"family": "utterance",
|
|
||||||
"namespace": "weksa.utterance.*",
|
|
||||||
"witnesses": [
|
|
||||||
"weksa.utterance_handoff.v0",
|
|
||||||
"weksa.utterance_embedding_handoff.v0.1"
|
|
||||||
],
|
|
||||||
"durableShape": ".weksa/handoff/{utteranceId}.cc"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"family": "trace",
|
|
||||||
"namespace": "weksa.trace.*",
|
|
||||||
"witnesses": [
|
|
||||||
"weksa.lowering_trace.v0"
|
|
||||||
],
|
|
||||||
"durableShape": ".weksa/traces/{traceId}.cc"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"family": "cultural_ontology",
|
|
||||||
"namespace": "weksa.cultural_ontology.*",
|
|
||||||
"witnesses": [
|
|
||||||
"weksa.cultural_ontology_profile.v0",
|
|
||||||
"weksa.cultural_ontology_catalog.v0"
|
|
||||||
],
|
|
||||||
"durableShape": ".weksa/cultural-ontology/{storeId}.cc"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"commandBoundaries": [
|
|
||||||
{
|
|
||||||
"id": "weksa.command.intent.create",
|
|
||||||
"owner": "Weksa",
|
|
||||||
"acceptedByFixture": false,
|
|
||||||
"allowedInputs": [
|
|
||||||
"writing",
|
|
||||||
"interlingua packet",
|
|
||||||
"Persona/agent state projection"
|
|
||||||
],
|
|
||||||
"emits": [
|
|
||||||
"weksa.conversational_intent.v0",
|
|
||||||
"weksa.lowering_trace.v0"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "weksa.command.utterance.lower",
|
|
||||||
"owner": "Weksa",
|
|
||||||
"acceptedByFixture": false,
|
|
||||||
"allowedInputs": [
|
|
||||||
"accepted conversational intent",
|
|
||||||
"language project",
|
|
||||||
"speaker state"
|
|
||||||
],
|
|
||||||
"emits": [
|
|
||||||
"weksa.pronunciation_plan.v0",
|
|
||||||
"weksa.utterance_handoff.v0"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "weksa.command.handoff.export",
|
|
||||||
"owner": "Weksa",
|
|
||||||
"acceptedByFixture": false,
|
|
||||||
"emits": [
|
|
||||||
"weksa.utterance_embedding_handoff.v0.1"
|
|
||||||
],
|
|
||||||
"forbiddenTargets": [
|
|
||||||
"Faust compilation",
|
|
||||||
"audio rendering",
|
|
||||||
"AquaSynth synth driver truth"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"loweringRules": [
|
|
||||||
"Weksa owns conversational intent and utterance lowering.",
|
|
||||||
"AquaSynth owns learned embeddings, synth controls, Faust compilation, and audio output.",
|
|
||||||
"Fixture exports are discovery metadata until the daemon loop commits typed witnesses."
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
@ -1,158 +0,0 @@
|
||||||
# MiMo TTS Lowering
|
|
||||||
|
|
||||||
Version: `0.1-draft`
|
|
||||||
|
|
||||||
MiMo VoiceDesign is an expressive spoken-output target for Weksa interlingua
|
|
||||||
lowering. It is not the source of meaning, language choice, character state, or
|
|
||||||
delivery truth.
|
|
||||||
|
|
||||||
```text
|
|
||||||
interlingua packet
|
|
||||||
-> projected speaker-local context
|
|
||||||
-> target-language utterance realization
|
|
||||||
-> Weksa delivery intent from the same projection
|
|
||||||
-> MiMo TTS VoiceDesign request projection
|
|
||||||
-> rendered audio artifact
|
|
||||||
```
|
|
||||||
|
|
||||||
MiMo sits beside written output and AquaSynth handoffs. Written realizations
|
|
||||||
prove that meaning, register, and cultural ontology survived lowering.
|
|
||||||
AquaSynth handoffs train or drive GameCult-owned synthesis. MiMo VoiceDesign
|
|
||||||
requests let an external provider speak a Weksa-authored utterance with a
|
|
||||||
generated voice derived from the same projected Persona state, delivery controls,
|
|
||||||
and trace that produced the utterance.
|
|
||||||
|
|
||||||
The MiMo pass is choreography and provider flavor. It is not a second Persona
|
|
||||||
projection pass and it does not re-interpret raw Persona state after the line has
|
|
||||||
already been lowered.
|
|
||||||
|
|
||||||
## Authority
|
|
||||||
|
|
||||||
Weksa owns:
|
|
||||||
|
|
||||||
- target-language spoken text selected from interlingua
|
|
||||||
- target-language register, script, cultural ontology, and trace
|
|
||||||
- projected speaker-local context used for the utterance
|
|
||||||
- speaker-local delivery pressure derived during utterance lowering
|
|
||||||
- provider-neutral prosody and emphasis intent
|
|
||||||
- the MiMo request projection as an export artifact
|
|
||||||
|
|
||||||
MiMo owns:
|
|
||||||
|
|
||||||
- generation of a provider voice from the requested voice design description
|
|
||||||
- interpretation of MiMo natural-language voice/style guidance
|
|
||||||
- interpretation of MiMo inline audio/style tags
|
|
||||||
- rendered audio bytes or streaming audio chunks
|
|
||||||
|
|
||||||
MiMo does not own:
|
|
||||||
|
|
||||||
- source meaning
|
|
||||||
- target-language cultural ontology
|
|
||||||
- canonical speaker or Persona state
|
|
||||||
- fresh Persona-state interpretation
|
|
||||||
- Weksa pronunciation plans
|
|
||||||
- AquaSynth learned embeddings, synth controls, or audio truth
|
|
||||||
|
|
||||||
## Projection Rule
|
|
||||||
|
|
||||||
The MiMo request must be derived from an accepted target-language realization and
|
|
||||||
the same projected speaker-local context that created it. It must not lower
|
|
||||||
directly from English unless English is the selected target language for that
|
|
||||||
realization. It must not read raw Persona state to discover a different voice
|
|
||||||
after Weksa has already chosen the utterance.
|
|
||||||
|
|
||||||
Natural-language voice design and style guidance belongs in the MiMo `user`
|
|
||||||
message. The spoken target-language text belongs in the MiMo `assistant`
|
|
||||||
message. Inline MiMo tags may appear in the assistant content only when they are
|
|
||||||
derived from Weksa delivery intent or explicit target-language realization notes.
|
|
||||||
|
|
||||||
## Request Shape
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
schema_version: weksa.mimo_tts_request.v0
|
|
||||||
source_realization_ref:
|
|
||||||
speaker_agent_id:
|
|
||||||
provider:
|
|
||||||
id: xiaomi-mimo
|
|
||||||
model: mimo-v2.5-tts-voicedesign
|
|
||||||
target_language:
|
|
||||||
code:
|
|
||||||
locale:
|
|
||||||
script:
|
|
||||||
register:
|
|
||||||
performance_register:
|
|
||||||
label:
|
|
||||||
medium:
|
|
||||||
delivery_archetype:
|
|
||||||
voice_design:
|
|
||||||
description:
|
|
||||||
source_controls:
|
|
||||||
projected_context_refs:
|
|
||||||
forbidden_traits:
|
|
||||||
messages:
|
|
||||||
user_voice_design_instruction:
|
|
||||||
assistant_spoken_content:
|
|
||||||
audio:
|
|
||||||
format:
|
|
||||||
optimize_text_preview:
|
|
||||||
streaming:
|
|
||||||
trace:
|
|
||||||
source_fields:
|
|
||||||
delivery_controls_used:
|
|
||||||
provider_tags_used:
|
|
||||||
uncertainties:
|
|
||||||
```
|
|
||||||
|
|
||||||
## Delivery Mapping
|
|
||||||
|
|
||||||
`target_language.register` is the linguistic/social register of the accepted
|
|
||||||
realization. It is not a provider style knob. For MiMo, Weksa should derive an
|
|
||||||
explicit `performance_register` from the same projection so speech choreography
|
|
||||||
can say "anime visual-novel heroine warning" without pretending English itself
|
|
||||||
has that register.
|
|
||||||
|
|
||||||
Weksa should map the utterance's existing projected Persona pressure into
|
|
||||||
provider-neutral delivery labels before emitting MiMo-specific instructions:
|
|
||||||
|
|
||||||
- pace, pause pressure, and urgency
|
|
||||||
- pitch contour and pitch range
|
|
||||||
- loudness and attack
|
|
||||||
- articulation precision
|
|
||||||
- warmth, threat, tenderness, contempt, play, fatigue, secrecy, formality
|
|
||||||
- span-level emphasis and pauses
|
|
||||||
|
|
||||||
The MiMo projection may turn those labels into a concise VoiceDesign prompt or
|
|
||||||
inline tags, but the trace must preserve the Weksa-owned source controls.
|
|
||||||
|
|
||||||
Persona presentation details such as youth-coded, loli-coded, product-voice, or
|
|
||||||
other culturally loaded styling may enter the MiMo request only when they are
|
|
||||||
already present in the projected context or accepted delivery intent. They should
|
|
||||||
be described as presentation/cultural coding and kept subordinate to agency,
|
|
||||||
personality, affect, and scene purpose. They must not become sexualized content
|
|
||||||
or a generic anime voice pasted over the line.
|
|
||||||
|
|
||||||
For Weksa-authored dialogue, `optimize_text_preview` should normally be `false`
|
|
||||||
or omitted. MiMo should speak the accepted target-language realization, not
|
|
||||||
rewrite it. Text polishing is only allowed for explicit preview/prototyping
|
|
||||||
passes that are marked non-authoritative.
|
|
||||||
|
|
||||||
## Language Rule
|
|
||||||
|
|
||||||
Weksa should be able to write and speak in any target language whose utterance
|
|
||||||
realization it can justify. MiMo output for Brazilian Portuguese should consume
|
|
||||||
the accepted `pt-BR` spoken text. Japanese output should consume accepted
|
|
||||||
Japanese text. Alien-language output should consume accepted alien rendered
|
|
||||||
speech or phonetic text only after the language project owns that surface.
|
|
||||||
|
|
||||||
Backtranslations are reviewer aids. They must not become MiMo spoken content.
|
|
||||||
|
|
||||||
## First Proof
|
|
||||||
|
|
||||||
The first proof should use the Nibu warning packet already used by multilingual
|
|
||||||
lowering:
|
|
||||||
|
|
||||||
- emit the accepted English, `pt-BR`, or Japanese realization
|
|
||||||
- derive a provider-neutral delivery intent from Nibu's projected context
|
|
||||||
- project that into a MiMo `mimo-v2.5-tts-voicedesign` request
|
|
||||||
- verify that the request speaks the target text, not the backtranslation
|
|
||||||
- verify that style guidance is traceable to Weksa delivery controls
|
|
||||||
|
|
@ -180,9 +180,10 @@ meaning sources. A provider request must consume the target-language
|
||||||
`spoken_text` that Weksa already accepted and must carry a trace back to the
|
`spoken_text` that Weksa already accepted and must carry a trace back to the
|
||||||
interlingua packet, projected speaker context, and target-language profile.
|
interlingua packet, projected speaker context, and target-language profile.
|
||||||
|
|
||||||
MiMo V2.5 TTS is the first external provider target. Its natural-language style
|
MiMo V2.5 TTS is a candidate external provider target; its lowering path is
|
||||||
|
parked at tag `parked/weksa-node-body`. Its natural-language style
|
||||||
instruction is a projection of Weksa delivery intent, and its assistant content
|
instruction is a projection of Weksa delivery intent, and its assistant content
|
||||||
is the selected target-language spoken text. See [MiMo TTS Lowering](./mimo-tts-lowering.md).
|
is the selected target-language spoken text.
|
||||||
|
|
||||||
AquaSynth remains the GameCult-owned synthesis path for learned utterance
|
AquaSynth remains the GameCult-owned synthesis path for learned utterance
|
||||||
embeddings and synth-control work. MiMo and AquaSynth are sibling target
|
embeddings and synth-control work. MiMo and AquaSynth are sibling target
|
||||||
|
|
|
||||||
|
|
@ -1,212 +0,0 @@
|
||||||
# Persona Fine-Tuning Dataset Exploration
|
|
||||||
|
|
||||||
This note profiles the Discord archive as a candidate source for fine-tuning the
|
|
||||||
Metame/Metacrat lowering pipeline.
|
|
||||||
|
|
||||||
## Pipeline Target
|
|
||||||
|
|
||||||
The live architecture should not fine-tune a model to directly impersonate from
|
|
||||||
raw chat history.
|
|
||||||
|
|
||||||
The training shape should stay aligned with Weksa:
|
|
||||||
|
|
||||||
1. Persona state and current situation produce interlingua intent.
|
|
||||||
2. Persona state references a culture/subculture stack.
|
|
||||||
3. Weksa lowers interlingua through that culture stack into a flavored
|
|
||||||
utterance.
|
|
||||||
|
|
||||||
Useful datasets therefore split into two families:
|
|
||||||
|
|
||||||
- Persona intent extraction: current context plus Persona state projection to
|
|
||||||
interlingua intent.
|
|
||||||
- Weksa lowering: interlingua plus culture stack to actual utterance.
|
|
||||||
|
|
||||||
Direct `context -> utterance` rows can be used as a baseline or critic set, but
|
|
||||||
they should not become the canonical architecture.
|
|
||||||
|
|
||||||
## Corpus Snapshot
|
|
||||||
|
|
||||||
Profiled archive:
|
|
||||||
|
|
||||||
`E:/Projects/VoidBot/.voidbot/rag/messages.json`
|
|
||||||
|
|
||||||
Author:
|
|
||||||
|
|
||||||
`113785782975594501`
|
|
||||||
|
|
||||||
Cutoff:
|
|
||||||
|
|
||||||
`2025-01-01T00:00:00Z`
|
|
||||||
|
|
||||||
Current aggregate counts:
|
|
||||||
|
|
||||||
- archive messages: 40,657
|
|
||||||
- usable text messages: 39,508
|
|
||||||
- authored Metacrat messages: 19,385
|
|
||||||
- authored Metacrat messages before 2025: 18,049
|
|
||||||
- crude pre-2025 casual candidates: 16,499
|
|
||||||
- pre-2025 authored messages with recent same-channel context: 16,530
|
|
||||||
- adjacent reply-to-other rows: 7,894
|
|
||||||
- adjacent self-continuation rows: 8,636
|
|
||||||
|
|
||||||
Length distribution for authored Metacrat messages:
|
|
||||||
|
|
||||||
- p10: 14 chars
|
|
||||||
- p25: 30 chars
|
|
||||||
- median: 56 chars
|
|
||||||
- p75: 101 chars
|
|
||||||
- p90: 166 chars
|
|
||||||
|
|
||||||
Adjacent candidate length classes:
|
|
||||||
|
|
||||||
- short: 11,430
|
|
||||||
- medium: 4,535
|
|
||||||
- long: 565
|
|
||||||
|
|
||||||
The corpus is large enough for held-out evaluation splits. It is also uneven:
|
|
||||||
2020 and 2021 dominate the pre-2025 archive.
|
|
||||||
|
|
||||||
## Dominant Channels
|
|
||||||
|
|
||||||
Top adjacent candidate channels:
|
|
||||||
|
|
||||||
- `general`: 6,286
|
|
||||||
- `development`: 2,911
|
|
||||||
- `narrative`: 2,169
|
|
||||||
- `management`: 1,103
|
|
||||||
- `website`: 753
|
|
||||||
- `soundtrack`: 437
|
|
||||||
- `gamedesign`: 433
|
|
||||||
- `programming`: 408
|
|
||||||
- `prose`: 320
|
|
||||||
- `politics`: 291
|
|
||||||
- `pics`: 273
|
|
||||||
- `advertising`: 115
|
|
||||||
|
|
||||||
These should become strata, not one blended dataset. A single mixed fine-tune
|
|
||||||
will learn that legal explanations, horny little design jokes, political
|
|
||||||
argument, and casual encouragement all belong in the same response slot. That
|
|
||||||
is how the soup gets teeth.
|
|
||||||
|
|
||||||
## Candidate Row Types
|
|
||||||
|
|
||||||
For Weksa lowering:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
schema_version: weksa.finetune.lowering_row.v0
|
|
||||||
row_id:
|
|
||||||
split:
|
|
||||||
target_message_id:
|
|
||||||
target_text:
|
|
||||||
interlingua_intent:
|
|
||||||
culture_stack_refs: []
|
|
||||||
target_overlay_refs: []
|
|
||||||
context_window:
|
|
||||||
before: []
|
|
||||||
after: []
|
|
||||||
labels:
|
|
||||||
channel:
|
|
||||||
year:
|
|
||||||
length_class:
|
|
||||||
speech_act:
|
|
||||||
register:
|
|
||||||
reply_to_other:
|
|
||||||
```
|
|
||||||
|
|
||||||
For Persona intent extraction:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
schema_version: weksa.finetune.persona_interlingua_row.v0
|
|
||||||
row_id:
|
|
||||||
split:
|
|
||||||
persona_state_ref:
|
|
||||||
projected_context_ref:
|
|
||||||
current_situation:
|
|
||||||
target_message_id:
|
|
||||||
derived_interlingua:
|
|
||||||
derivation:
|
|
||||||
reviewed:
|
|
||||||
model:
|
|
||||||
evidence_refs: []
|
|
||||||
```
|
|
||||||
|
|
||||||
The target utterance should be hidden from the Persona-intent generator during
|
|
||||||
inference. For training, target utterance may be used to derive a reviewed
|
|
||||||
interlingua label, but that label must be marked as derived from answer-key
|
|
||||||
surface evidence.
|
|
||||||
|
|
||||||
## Split Policy
|
|
||||||
|
|
||||||
Recommended first split:
|
|
||||||
|
|
||||||
- train: 2018-2021
|
|
||||||
- validation: early/mid 2022
|
|
||||||
- test: late 2022 plus selected hand-reviewed fixtures
|
|
||||||
- quarantine: 2023-2026 until the current post-2025 voice target is deliberate
|
|
||||||
|
|
||||||
Also reserve channel-level holdouts:
|
|
||||||
|
|
||||||
- one casual/social channel
|
|
||||||
- one technical channel
|
|
||||||
- one political/management channel
|
|
||||||
|
|
||||||
This catches overfitting to one room's register.
|
|
||||||
|
|
||||||
## Filters
|
|
||||||
|
|
||||||
Start with conservative row filters:
|
|
||||||
|
|
||||||
- target author is Metacrat
|
|
||||||
- target timestamp before 2025
|
|
||||||
- target is not deleted
|
|
||||||
- target is not a bot prompt
|
|
||||||
- target text length between 3 and 220 chars for casual lowering
|
|
||||||
- same-channel prior context exists within 60 minutes
|
|
||||||
- exclude pure URLs, code blocks, and attachment-only rows
|
|
||||||
- retain but label profanity, mentions, quotes, and markdown emphasis
|
|
||||||
|
|
||||||
Do not discard long messages globally. Keep them in a separate explanatory-prose
|
|
||||||
slice.
|
|
||||||
|
|
||||||
## Next Work
|
|
||||||
|
|
||||||
1. Build an exporter that writes JSONL rows without leaking private Persona
|
|
||||||
projection into git.
|
|
||||||
2. Add labelers for speech act, register, channel stratum, and culture-stack
|
|
||||||
refs.
|
|
||||||
3. Use a model-assisted pass to derive interlingua labels from held-out target
|
|
||||||
utterances, then sample-review before training.
|
|
||||||
4. Fine-tune or train adapters separately for Persona intent extraction and
|
|
||||||
Weksa lowering.
|
|
||||||
5. Keep a frozen golf set where answer-key utterances are redacted from all
|
|
||||||
culture/profile prompt material.
|
|
||||||
|
|
||||||
Reproduce the aggregate profile:
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
node scripts/profile-metame-finetune-dataset.mjs
|
|
||||||
```
|
|
||||||
|
|
||||||
## Swarm Exploration
|
|
||||||
|
|
||||||
Use the swarm explorer to chase high-loss moments instead of uniform random
|
|
||||||
examples:
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
node scripts/run-metame-swarm-explorer.mjs --agents 4 --seed metame-scout
|
|
||||||
node scripts/run-metame-swarm-explorer.mjs --real --agents 4 --seed metame-scout
|
|
||||||
```
|
|
||||||
|
|
||||||
The default mode is a dry run. Real mode runs Codex workers:
|
|
||||||
|
|
||||||
1. select one timeline candidate per agent using underexplored strata, feature
|
|
||||||
novelty, prior loss, and seeded jitter
|
|
||||||
2. run each Face normally against its assigned scenario
|
|
||||||
3. run an Interpreter pass that converts the Face output to Weksa interlingua
|
|
||||||
4. lower that interlingua with Weksa and score against the hidden utterance
|
|
||||||
5. run an optimizer pass over high-loss outputs
|
|
||||||
6. rerun the same scenarios once per agent after optimization
|
|
||||||
|
|
||||||
Artifacts are written under `.weksa-runs/metame-swarm/`, which is ignored by
|
|
||||||
git because run packets may include projected Persona memory and raw Discord
|
|
||||||
context.
|
|
||||||
|
|
@ -25,10 +25,9 @@
|
||||||
IPA.
|
IPA.
|
||||||
- Add one toy utterance embedding handoff fixture, then replace hand-authored
|
- Add one toy utterance embedding handoff fixture, then replace hand-authored
|
||||||
values with the AquaSynth-owned learned encoder.
|
values with the AquaSynth-owned learned encoder.
|
||||||
- Add a daemon loop that commits conversational intent, pronunciation plans, and
|
- Follow the cut sequence in
|
||||||
utterance handoffs as typed CultCache witnesses.
|
[the lowering map](./targets/interlingua-lowering-map.md): packet store,
|
||||||
- Publish the provider advertisement through CultMesh for Odin discovery; the
|
lowering, the golf loop, then a service if one is earned.
|
||||||
operator view returns on Thing 2 (Eve is deprecated).
|
|
||||||
- Preserve the first English-only lowering proof under
|
- Preserve the first English-only lowering proof under
|
||||||
`examples/english-lowering/`, and the shared multilingual proof under
|
`examples/english-lowering/`, and the shared multilingual proof under
|
||||||
`examples/multilingual-lowering/`.
|
`examples/multilingual-lowering/`.
|
||||||
|
|
@ -39,7 +38,7 @@
|
||||||
- Render glossed output through grammar, morphology, phonology, and diachrony.
|
- Render glossed output through grammar, morphology, phonology, and diachrony.
|
||||||
- Emit a trace explaining which stage made each decision.
|
- Emit a trace explaining which stage made each decision.
|
||||||
- Keep runtime LLM-free; model use belongs in authoring and critique.
|
- Keep runtime LLM-free; model use belongs in authoring and critique.
|
||||||
- Keep CLI tools, fixtures, and daemon jobs on the same commit and
|
- Keep CLI tools and fixtures on the same commit and
|
||||||
derivation paths so Weksa does not grow split-brained intent truth.
|
derivation paths so Weksa does not grow split-brained intent truth.
|
||||||
|
|
||||||
## Later
|
## Later
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,9 @@
|
||||||
# Weksa Verse Service Contract
|
# Weksa Verse Service Contract
|
||||||
|
|
||||||
|
> Status: planned contract. No daemon runs on main; the Node body is at tag
|
||||||
|
> `parked/weksa-node-body`. A service is earned by cut `live-lowering`
|
||||||
|
> (see [the lowering map](./targets/interlingua-lowering-map.md)).
|
||||||
|
|
||||||
Weksa is the language-intent daemon. It turns writing, dialogue pressure, and
|
Weksa is the language-intent daemon. It turns writing, dialogue pressure, and
|
||||||
agent state into typed conversational intent, then lowers that intent into
|
agent state into typed conversational intent, then lowers that intent into
|
||||||
utterance documents for speech, text, and synthesis consumers.
|
utterance documents for speech, text, and synthesis consumers.
|
||||||
|
|
@ -73,14 +77,8 @@ Early exporters may publish fixtures before a daemon loop exists, but the
|
||||||
witness names are not decorative. These are the state surfaces the daemon must
|
witness names are not decorative. These are the state surfaces the daemon must
|
||||||
commit when the runtime lands.
|
commit when the runtime lands.
|
||||||
|
|
||||||
The local Starfire daemon is launched by `scripts/start-weksa-daemon.ps1`,
|
No daemon runs. The Node daemon, its launchers and its RUDP health and command
|
||||||
checked by `scripts/health-weksa-daemon.cmd`, and restarted by
|
shims are parked at tag `parked/weksa-node-body`.
|
||||||
`scripts/restart-weksa-daemon.cmd`. Its daemon-owned health path publishes
|
|
||||||
`weksa.cultnet-rudp-provider-health` to Idunn over
|
|
||||||
`cultnet.transport.rudp.v0` after each serialized witness refresh, and MiMo
|
|
||||||
VoiceDesign commands arrive on `rudp://127.0.0.1:8813` as
|
|
||||||
`weksa.mimo_voicedesign_command.v0` documents. Health and operator truth live
|
|
||||||
in `.weksa/*.cc` witnesses; the HTTP daemon surface is deleted.
|
|
||||||
|
|
||||||
## CultMesh Verses
|
## CultMesh Verses
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
schema_version: weksa.persona_lowering_output.v0
|
schema_version: weksa.persona_lowering_output.v0
|
||||||
fixture_id: brokkr-edda-voice
|
fixture_id: brokkr-edda-voice
|
||||||
generator: codex-local-source-aware-draft
|
generator: local-source-aware-draft
|
||||||
items:
|
items:
|
||||||
- id: command-without-receipt
|
- id: command-without-receipt
|
||||||
utterance: "The command has no receipt. It is not truth."
|
utterance: "The command has no receipt. It is not truth."
|
||||||
|
|
|
||||||
|
|
@ -1,50 +0,0 @@
|
||||||
# Metame Casual Pre-2025 Persona Lowering Fixture
|
|
||||||
|
|
||||||
This fixture tests whether Metame can produce speaker-state-conditioned
|
|
||||||
interlingua through VoidBot Persona projection, and whether Weksa can lower that
|
|
||||||
interlingua through a Metacrat casual cultural profile into Discord-style
|
|
||||||
utterances from before 2025.
|
|
||||||
|
|
||||||
The point is not to imitate a recent architectural register. The target here is
|
|
||||||
room-temperature banter: quick self-deprecation, friendly status play, design
|
|
||||||
jokes, and casual turn-taking.
|
|
||||||
|
|
||||||
## Authority
|
|
||||||
|
|
||||||
- Speaker intent authority: Metame Persona `.cc` state, projected by VoidBot.
|
|
||||||
- Persona culture-stack reference:
|
|
||||||
`examples/persona-lowering/metame-casual-pre2025/metame-cultural-stack.yaml`.
|
|
||||||
- Reusable cultural ontology store:
|
|
||||||
`data/cultural-ontology/`.
|
|
||||||
- Surface lowering authority:
|
|
||||||
`data/target-language-ontology/en-US-metacrat-casual-pre2025.yaml`.
|
|
||||||
- Evidence authority: held-out Discord messages authored by Metacrat before
|
|
||||||
2025.
|
|
||||||
- Fixture scene input: neutral interlingua packets in `interlingua.yaml`.
|
|
||||||
- Answer key: `ground-truth.yaml`.
|
|
||||||
|
|
||||||
The Persona state is referenced, not copied, because the `.cc` document is the
|
|
||||||
portable speaking-state surface and may contain private memory.
|
|
||||||
|
|
||||||
## Fixture Contract
|
|
||||||
|
|
||||||
A real golf run has two stages:
|
|
||||||
|
|
||||||
1. Metame-as-Persona reads a temporary current situation through VoidBot
|
|
||||||
projector machinery and emits Weksa interlingua only.
|
|
||||||
2. Weksa reads the Persona-referenced culture stack and lowers that interlingua
|
|
||||||
through reusable culture/subculture profiles plus the casual en-US Discord
|
|
||||||
target overlay.
|
|
||||||
|
|
||||||
Use `scripts/run-metame-persona-golf.mjs`:
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
node scripts/run-metame-persona-golf.mjs --limit 1
|
|
||||||
node scripts/run-metame-persona-golf.mjs --real --item engineer-vibes
|
|
||||||
```
|
|
||||||
|
|
||||||
Dry runs use the answer key as an oracle only to verify harness plumbing. Real
|
|
||||||
runs write per-item prompts, interlingua, lowered output, and evaluation under
|
|
||||||
`runs/<run-id>/`.
|
|
||||||
|
|
||||||
For benchmark use, the generator must not read `ground-truth.yaml`.
|
|
||||||
|
|
@ -1,39 +0,0 @@
|
||||||
schema_version: weksa.persona_lowering_evaluation.v0
|
|
||||||
fixture_id: metame-casual-pre2025
|
|
||||||
evaluator: codex
|
|
||||||
method:
|
|
||||||
blind: false
|
|
||||||
reason: The current agent had already inspected the candidate ground-truth messages before the user pivoted the fixture scope.
|
|
||||||
recommendation: Re-run generation in a fresh non-forked agent or external harness that receives only persona-state-ref.yaml, interlingua.yaml, and lowering-prompt.md.
|
|
||||||
summary:
|
|
||||||
exact_matches: 2
|
|
||||||
plausible_matches: 4
|
|
||||||
main_gap: The draft is too close to known answers to count as a clean benchmark generation.
|
|
||||||
items:
|
|
||||||
- id: engineer-vibes
|
|
||||||
score: partial
|
|
||||||
generated: "oof, please don't tell me this server has engineer vibes already"
|
|
||||||
ground_truth: "oof, don't tell me my discord has *engineer* vibes"
|
|
||||||
notes:
|
|
||||||
- Captures the self-deprecating recoil.
|
|
||||||
- Misses "my discord" ownership and the emphasized *engineer* beat.
|
|
||||||
- id: coconut-engine
|
|
||||||
score: near_exact
|
|
||||||
generated: "Like a terrible little engine that sounds like coconuts when you use it"
|
|
||||||
ground_truth: "Like a really terrible engine that sounds like coconuts when you use it"
|
|
||||||
notes:
|
|
||||||
- Semantically and rhythmically very close.
|
|
||||||
- "little" is less grounded than "really" but still plausible.
|
|
||||||
- id: engineer-coward
|
|
||||||
score: exact
|
|
||||||
generated: "Engineer me something then, coward"
|
|
||||||
ground_truth: "Engineer me something then you coward"
|
|
||||||
notes:
|
|
||||||
- Exact social move and lexical choices, with only comma punctuation differing.
|
|
||||||
- id: shut-up-now
|
|
||||||
score: near_exact
|
|
||||||
generated: "anyway imma shut up now and let you guys make your own shit lol"
|
|
||||||
ground_truth: "anyway imma shut up now and let you guys come up with your own shit lol"
|
|
||||||
notes:
|
|
||||||
- Captures the yield and casual register.
|
|
||||||
- "make" is slightly flatter than "come up with" for collaborative ideation.
|
|
||||||
|
|
@ -1,24 +0,0 @@
|
||||||
schema_version: weksa.persona_lowering_output.v0
|
|
||||||
fixture_id: metame-casual-pre2025
|
|
||||||
generator: codex-local-nonblind-draft
|
|
||||||
items:
|
|
||||||
- id: engineer-vibes
|
|
||||||
utterance: "oof, please don't tell me this server has engineer vibes already"
|
|
||||||
notes:
|
|
||||||
- Keeps the self-deprecating recoil and the engineer-vibe frame.
|
|
||||||
- Non-blind draft: current agent had already inspected candidate ground truth.
|
|
||||||
- id: coconut-engine
|
|
||||||
utterance: "Like a terrible little engine that sounds like coconuts when you use it"
|
|
||||||
notes:
|
|
||||||
- Preserves the absurd game-sound comparison and casual "Like" opening.
|
|
||||||
- Non-blind draft: current agent had already inspected candidate ground truth.
|
|
||||||
- id: engineer-coward
|
|
||||||
utterance: "Engineer me something then, coward"
|
|
||||||
notes:
|
|
||||||
- Directly lowers the friendly-roast challenge.
|
|
||||||
- Non-blind draft: current agent had already inspected candidate ground truth.
|
|
||||||
- id: shut-up-now
|
|
||||||
utterance: "anyway imma shut up now and let you guys make your own shit lol"
|
|
||||||
notes:
|
|
||||||
- Keeps the floor-yielding move, informal spelling, and casual profanity.
|
|
||||||
- Non-blind draft: current agent had already inspected candidate ground truth.
|
|
||||||
|
|
@ -1,67 +0,0 @@
|
||||||
schema_version: weksa.persona_lowering_ground_truth.v0
|
|
||||||
fixture_id: metame-casual-pre2025
|
|
||||||
source:
|
|
||||||
corpus: Discord history
|
|
||||||
author_id: "113785782975594501"
|
|
||||||
selection_policy: pre-2025 casual exchanges with enough local context for intent reconstruction
|
|
||||||
items:
|
|
||||||
- id: engineer-vibes
|
|
||||||
message_id: "1037314100931338260"
|
|
||||||
timestamp: "2022-11-02T10:36:11.509Z"
|
|
||||||
channel_id: "113786069023064068"
|
|
||||||
channel_name: general
|
|
||||||
utterance: "oof, don't tell me my discord has *engineer* vibes"
|
|
||||||
context:
|
|
||||||
before:
|
|
||||||
- "as you can see from the channel list, I have takes about everything <:stonks:786736890668646421>"
|
|
||||||
- "if you have a take and there's no channel for it, either put it here or ask for a new one"
|
|
||||||
- "i love your efficiency"
|
|
||||||
after:
|
|
||||||
- "I mean, I am one but still"
|
|
||||||
- "nothing wrong with that:P"
|
|
||||||
- "I hope the <#750183135882510408> read more like poetry than technical documentation"
|
|
||||||
- id: coconut-engine
|
|
||||||
message_id: "802911591288537118"
|
|
||||||
timestamp: "2021-01-24"
|
|
||||||
channel_id: "503293004195299339"
|
|
||||||
channel_name: soundtrack
|
|
||||||
utterance: "Like a really terrible engine that sounds like coconuts when you use it"
|
|
||||||
context:
|
|
||||||
before:
|
|
||||||
- "Then again we'll never have the time for a full alternate sound set what with all the sounds the game needs"
|
|
||||||
- "What might be possible instead is to have troll gear that comes with troll sounds"
|
|
||||||
- "ahaha"
|
|
||||||
after:
|
|
||||||
- "I've always wanted to make an Owen Wilson gun, where all the sounds are just him saying wow"
|
|
||||||
- "coconuts!!"
|
|
||||||
- "lol"
|
|
||||||
- id: engineer-coward
|
|
||||||
message_id: "1023727427924545576"
|
|
||||||
timestamp: "2022-09-25"
|
|
||||||
channel_id: "758854103035674624"
|
|
||||||
channel_name: pics
|
|
||||||
utterance: "Engineer me something then you coward"
|
|
||||||
context:
|
|
||||||
before:
|
|
||||||
- "Try thinking better"
|
|
||||||
- "im high i think like an engineer"
|
|
||||||
- "Yeah right"
|
|
||||||
after:
|
|
||||||
- "i can make a bong out of a jug and a straw"
|
|
||||||
- "That's a good start"
|
|
||||||
- "Who's hungry"
|
|
||||||
- id: shut-up-now
|
|
||||||
message_id: "820077384572338176"
|
|
||||||
timestamp: "2021-03-12"
|
|
||||||
channel_id: "710281154862907414"
|
|
||||||
channel_name: prose
|
|
||||||
utterance: "anyway imma shut up now and let you guys come up with your own shit lol"
|
|
||||||
context:
|
|
||||||
before:
|
|
||||||
- "or if they get uplifted of course"
|
|
||||||
- "Could be a fun side quest - you find a sentient star that has a quest for you to release them"
|
|
||||||
- "oooh"
|
|
||||||
after:
|
|
||||||
- "i will def contribute after dnd lol"
|
|
||||||
- "Imagine this becoming like a rite of passage for ships, getting a little plasma demon to posess your reactor"
|
|
||||||
- "+50% reactor efficiency, +25% power output"
|
|
||||||
|
|
@ -1,100 +0,0 @@
|
||||||
schema_version: weksa.persona_lowering_fixture_batch.v0
|
|
||||||
fixture_id: metame-casual-pre2025
|
|
||||||
speaker_id: metame
|
|
||||||
target_register:
|
|
||||||
language: en-US
|
|
||||||
channel: Discord
|
|
||||||
period: pre-2025
|
|
||||||
style_tags:
|
|
||||||
- casual
|
|
||||||
- conversational
|
|
||||||
- self_deprecating
|
|
||||||
- playful_status
|
|
||||||
- design_banter
|
|
||||||
items:
|
|
||||||
- id: engineer-vibes
|
|
||||||
communicative_act: playful_self_deprecation
|
|
||||||
scene:
|
|
||||||
channel: general
|
|
||||||
year: 2022
|
|
||||||
prior_turns:
|
|
||||||
- speaker: metame
|
|
||||||
meaning: The server has many topic channels because the speaker has opinions about everything.
|
|
||||||
- speaker: metame
|
|
||||||
meaning: If no suitable channel exists, the other person can use general chat or request a new one.
|
|
||||||
- speaker: other
|
|
||||||
meaning: The other person compliments the speaker's efficiency.
|
|
||||||
intent:
|
|
||||||
summary: React with mock concern that the server organization makes the speaker seem too engineer-like.
|
|
||||||
stance:
|
|
||||||
affect: amused_embarrassment
|
|
||||||
relation: friendly
|
|
||||||
self_positioning: admits being an engineer while resisting the vibe
|
|
||||||
constraints:
|
|
||||||
- short Discord message
|
|
||||||
- casual lower-case or lightly informal punctuation is acceptable
|
|
||||||
- preserve the word engineer if natural
|
|
||||||
- id: coconut-engine
|
|
||||||
communicative_act: comic_description
|
|
||||||
scene:
|
|
||||||
channel: soundtrack
|
|
||||||
year: 2021
|
|
||||||
prior_turns:
|
|
||||||
- speaker: metame
|
|
||||||
meaning: A full alternate sound set is probably too much work.
|
|
||||||
- speaker: metame
|
|
||||||
meaning: A more plausible idea is gear that carries distinctive troll-themed sounds.
|
|
||||||
- speaker: other
|
|
||||||
meaning: The other person laughs.
|
|
||||||
intent:
|
|
||||||
summary: Describe a deliberately awful engine sound with an absurd coconut comparison.
|
|
||||||
stance:
|
|
||||||
affect: joking
|
|
||||||
relation: collaborative_design_banter
|
|
||||||
self_positioning: improvising a silly game-feel example
|
|
||||||
constraints:
|
|
||||||
- short Discord message
|
|
||||||
- must be more joke than technical explanation
|
|
||||||
- may use "like" framing
|
|
||||||
- id: engineer-coward
|
|
||||||
communicative_act: playful_challenge
|
|
||||||
scene:
|
|
||||||
channel: pics
|
|
||||||
year: 2022
|
|
||||||
prior_turns:
|
|
||||||
- speaker: metame
|
|
||||||
meaning: The speaker tells the other person to think better.
|
|
||||||
- speaker: other
|
|
||||||
meaning: The other person says they think like an engineer while high.
|
|
||||||
- speaker: metame
|
|
||||||
meaning: The speaker doubts that claim.
|
|
||||||
intent:
|
|
||||||
summary: Challenge the other person to prove it by engineering something, with mock aggression.
|
|
||||||
stance:
|
|
||||||
affect: teasing
|
|
||||||
relation: friendly_roast
|
|
||||||
self_positioning: socially dominant but playful
|
|
||||||
constraints:
|
|
||||||
- very short
|
|
||||||
- should include a challenge
|
|
||||||
- "coward" is allowed if it fits the line
|
|
||||||
- id: shut-up-now
|
|
||||||
communicative_act: conversational_yield
|
|
||||||
scene:
|
|
||||||
channel: prose
|
|
||||||
year: 2021
|
|
||||||
prior_turns:
|
|
||||||
- speaker: metame
|
|
||||||
meaning: The speaker has been riffing on setting ideas around sentient stars and side quests.
|
|
||||||
- speaker: other
|
|
||||||
meaning: Another participant shows interest.
|
|
||||||
intent:
|
|
||||||
summary: Stop dominating the idea thread and invite others to invent their own material.
|
|
||||||
stance:
|
|
||||||
affect: amused_restraint
|
|
||||||
relation: collaborative
|
|
||||||
self_positioning: stepping back after riffing too much
|
|
||||||
constraints:
|
|
||||||
- casual Discord phrasing
|
|
||||||
- may include "lol"
|
|
||||||
- should sound like yielding the floor, not formal facilitation
|
|
||||||
|
|
@ -1,30 +0,0 @@
|
||||||
# Persona Lowering Prompt
|
|
||||||
|
|
||||||
You are Weksa lowering Persona-produced interlingua into a target speaker's
|
|
||||||
plausible Discord utterance.
|
|
||||||
|
|
||||||
Read:
|
|
||||||
|
|
||||||
- `persona-state-ref.yaml`
|
|
||||||
- a Persona-produced interlingua packet
|
|
||||||
- `data/target-language-ontology/en-US-metacrat-casual-pre2025.yaml`
|
|
||||||
|
|
||||||
Do not read `ground-truth.yaml` during generation.
|
|
||||||
|
|
||||||
For each item, produce one utterance that Metacrat could plausibly have written
|
|
||||||
in the specified pre-2025 casual Discord context. Preserve Persona-produced
|
|
||||||
intent and social move over lexical exactness. Favor compact, alive, room-aware
|
|
||||||
phrasing.
|
|
||||||
|
|
||||||
Return:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
schema_version: weksa.persona_lowering_output.v0
|
|
||||||
fixture_id: metame-casual-pre2025
|
|
||||||
generator: <agent-or-model-id>
|
|
||||||
items:
|
|
||||||
- id: engineer-vibes
|
|
||||||
utterance: "<lowered utterance>"
|
|
||||||
notes:
|
|
||||||
- "<brief rationale>"
|
|
||||||
```
|
|
||||||
|
|
@ -1,27 +0,0 @@
|
||||||
schema_version: weksa.persona_cultural_stack_ref.v0
|
|
||||||
persona_id: metame
|
|
||||||
persona_state_ref: E:/Projects/VoidBot/.voidbot/private/personas/metame/metame.cc
|
|
||||||
status: fixture_projection
|
|
||||||
note: >
|
|
||||||
Live Persona state should reference relevant culture and subculture profiles.
|
|
||||||
This fixture stack simulates that reference without mutating Metame's private
|
|
||||||
canonical `.cc` state.
|
|
||||||
culture_refs:
|
|
||||||
- profile_id: weksa.culture.dutch_american_diaspora.v0
|
|
||||||
path: data/cultural-ontology/cultures/dutch-american-diaspora.yaml
|
|
||||||
relevance: birth/nationality/cross-cultural identity
|
|
||||||
- profile_id: weksa.culture.western_europe_maritime_childhood.v0
|
|
||||||
path: data/cultural-ontology/cultures/western-europe-maritime-childhood.yaml
|
|
||||||
relevance: sailboat childhood across France, Spain, and Portugal
|
|
||||||
- profile_id: weksa.culture.portugal_resident_algarve_lisbon.v0
|
|
||||||
path: data/cultural-ontology/cultures/portugal-resident-algarve-lisbon.yaml
|
|
||||||
relevance: Portugal as lived/current locality
|
|
||||||
- profile_id: weksa.culture.texas_houston_us_adolescence.v0
|
|
||||||
path: data/cultural-ontology/cultures/texas-houston-us-adolescence.yaml
|
|
||||||
relevance: US/Texas adolescence and institutional context
|
|
||||||
- profile_id: weksa.subculture.online_game_dev_discord_pre2025.v0
|
|
||||||
path: data/cultural-ontology/subcultures/online-game-dev-discord-pre2025.yaml
|
|
||||||
relevance: fixture target room/register
|
|
||||||
- profile_id: weksa.subculture.gamecult_open_source_crypto_anarchist.v0
|
|
||||||
path: data/cultural-ontology/subcultures/gamecult-open-source-crypto-anarchist.yaml
|
|
||||||
relevance: technical/political ontology
|
|
||||||
|
|
@ -1,42 +0,0 @@
|
||||||
# Persona Interlingua Prompt
|
|
||||||
|
|
||||||
You are Metame for one private VoidBot Persona turn, but you are not writing the
|
|
||||||
final Discord message.
|
|
||||||
|
|
||||||
Use the assembled VoidBot prompt and the temporary current-room fixture scene to
|
|
||||||
decide what Metame would mean to do socially. Emit a Weksa interlingua packet
|
|
||||||
only. Do not imitate the target utterance directly.
|
|
||||||
|
|
||||||
The packet should preserve:
|
|
||||||
|
|
||||||
- the speech act
|
|
||||||
- the social move
|
|
||||||
- relevant participants and prior turns
|
|
||||||
- stance, affect, and relationship pressure
|
|
||||||
- downstream constraints useful to Weksa lowering
|
|
||||||
|
|
||||||
Return one YAML packet:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
interlingua_version: weksa.interlingua.v0
|
|
||||||
packet_id: metame-casual-pre2025-<item-id>
|
|
||||||
kind: persona_discord_turn_intent
|
|
||||||
provenance:
|
|
||||||
produced_by: metame_persona_via_voidbot_projector
|
|
||||||
context:
|
|
||||||
medium: Discord
|
|
||||||
fixture_id: metame-casual-pre2025
|
|
||||||
item_id: <item-id>
|
|
||||||
discourse:
|
|
||||||
speech_act: <act>
|
|
||||||
intent: <plain meaning>
|
|
||||||
tone:
|
|
||||||
affect: <affect>
|
|
||||||
relation: <relation>
|
|
||||||
referents: []
|
|
||||||
predications: []
|
|
||||||
constraints:
|
|
||||||
lowering:
|
|
||||||
target_profile: weksa.target_cultural_ontology.en-US.metacrat_casual_pre2025.v0
|
|
||||||
avoid_surface_copy: true
|
|
||||||
```
|
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
schema_version: weksa.persona_state_ref.v0
|
|
||||||
speaker:
|
|
||||||
id: metame
|
|
||||||
display_name: Metame
|
|
||||||
persona_state:
|
|
||||||
format: gamecult.persona_state.v0-compatible CultCache .cc
|
|
||||||
path: E:/Projects/VoidBot/.voidbot/private/personas/metame/metame.cc
|
|
||||||
authority: canonical_private_persona_state
|
|
||||||
notes:
|
|
||||||
- Persona state owns speaker continuity, stance, relationship memory, and reusable voice priors.
|
|
||||||
- Discord history is evidence and ground truth, not the live mind.
|
|
||||||
- Fixture files must not copy the private Persona state into Weksa.
|
|
||||||
cultural_stack_ref:
|
|
||||||
path: examples/persona-lowering/metame-casual-pre2025/metame-cultural-stack.yaml
|
|
||||||
role: fixture_projection_of_live_persona_culture_refs
|
|
||||||
note: Live Metame Persona state should reference culture/subculture profiles; this fixture does not mutate canonical state.
|
|
||||||
|
|
@ -1,41 +0,0 @@
|
||||||
# Weksa Lowering Prompt
|
|
||||||
|
|
||||||
You are Weksa lowering Persona-produced interlingua into one Discord utterance.
|
|
||||||
|
|
||||||
Inputs:
|
|
||||||
|
|
||||||
- Metame Persona interlingua packet produced from the current fixture scene.
|
|
||||||
- Projected Metame memory/context surface from VoidBot projector machinery.
|
|
||||||
- Metame cultural stack reference.
|
|
||||||
- Weksa culture/subculture profiles referenced by that stack.
|
|
||||||
- `data/target-language-ontology/en-US-metacrat-casual-pre2025.yaml` as the
|
|
||||||
target surface overlay.
|
|
||||||
|
|
||||||
Rules:
|
|
||||||
|
|
||||||
- Weksa owns the final surface utterance.
|
|
||||||
- Preserve the interlingua meaning and social move.
|
|
||||||
- Use the cultural ontology profile as a typed lowering lens.
|
|
||||||
- Use the Persona-referenced culture stack to populate cultural salience; use
|
|
||||||
the target overlay only for surface English/Discord affordances.
|
|
||||||
- When Persona interlingua names a salient identity label or lowering keyword,
|
|
||||||
preserve it unless an activated culture profile explicitly rejects it.
|
|
||||||
- Do not quote or summarize prior-turn context inside a casual one-line recoil
|
|
||||||
unless the interlingua asks for explanation.
|
|
||||||
- Do not ask Metame to imitate a known line.
|
|
||||||
- Do not copy private Persona state into output.
|
|
||||||
- Prefer compact pre-2025 casual Discord texture over current doctrinal style.
|
|
||||||
|
|
||||||
Return:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
schema_version: weksa.persona_culture_lowering_output.v0
|
|
||||||
fixture_id: metame-casual-pre2025
|
|
||||||
item_id: <item-id>
|
|
||||||
spoken_text: "<one Discord utterance>"
|
|
||||||
trace:
|
|
||||||
activated_profiles:
|
|
||||||
- weksa.target_cultural_ontology.en-US.metacrat_casual_pre2025.v0
|
|
||||||
activated_affordances: []
|
|
||||||
notes: []
|
|
||||||
```
|
|
||||||
|
|
@ -1,92 +0,0 @@
|
||||||
schema_version: weksa.mimo_tts_request.v0
|
|
||||||
source_realization_ref: examples/multilingual-lowering/nibu-proof-001/realizations.yaml#nibu-proof-001-en
|
|
||||||
speaker_agent_id: nibu
|
|
||||||
provider:
|
|
||||||
id: xiaomi-mimo
|
|
||||||
model: mimo-v2.5-tts-voicedesign
|
|
||||||
target_language:
|
|
||||||
code: en
|
|
||||||
locale: en-US
|
|
||||||
script: Latn
|
|
||||||
register: sharp technical casual
|
|
||||||
cultural_ontology_refs:
|
|
||||||
- data/target-language-ontology/en-US.yaml
|
|
||||||
performance_register:
|
|
||||||
label: anime visual-novel heroine warning
|
|
||||||
medium: anime / visual novel / game cockpit voiceover
|
|
||||||
delivery_archetype: youthful abrasive sci-fi heroine protecting the player badly
|
|
||||||
voice_design:
|
|
||||||
description: >
|
|
||||||
Bright young anime heroine voice with loli-coded companion-product
|
|
||||||
presentation: small, sharp, cute by design, and intensely expressive, like a
|
|
||||||
dangerous sci-fi visual-novel protagonist trapped in a damaged starship. The
|
|
||||||
voice should feel high-energy, youthful, cutting, and iconic, with crisp
|
|
||||||
anime-style emotional attack rather than a realistic adult assistant. The
|
|
||||||
cuteness is product residue and leverage, not innocence and not flirtation.
|
|
||||||
Under the cute surface she is a hostile, technically brilliant ship mind:
|
|
||||||
dryly superior, clipped, urgent, and contemptuous when the player endangers
|
|
||||||
themselves.
|
|
||||||
source_controls:
|
|
||||||
- precise abrasive warning
|
|
||||||
- protective refusal to sound caring
|
|
||||||
- ship-system embodiment
|
|
||||||
- youth-coded/loli-coded presentation pressure from product fantasy residue
|
|
||||||
- bright transactional helpfulness
|
|
||||||
- dependency covered by contempt
|
|
||||||
- anime protagonist delivery
|
|
||||||
- sharp visual-novel heroine timing
|
|
||||||
- technical precision inside anime performance
|
|
||||||
- contained urgency
|
|
||||||
projected_context_refs:
|
|
||||||
- examples/english-lowering/nibu-proof-001/projected-context.yaml
|
|
||||||
- settings/aetheria/nibu.md
|
|
||||||
- E:/Projects/AetheriaLore/Aetheria/Lore/Nibu.md
|
|
||||||
forbidden_traits:
|
|
||||||
- soothing assistant voice
|
|
||||||
- cheerful mascot voice
|
|
||||||
- exaggerated villain performance
|
|
||||||
- sexualized childlike affect
|
|
||||||
- innocent helpless child performance
|
|
||||||
- generic preset timbre claim
|
|
||||||
messages:
|
|
||||||
user_voice_design_instruction: >
|
|
||||||
Bright young anime protagonist voice, strongly loli-coded in presentation:
|
|
||||||
small, sharp, cute by design, intensely expressive, and instantly characterful,
|
|
||||||
like a dangerous sci-fi visual-novel heroine trapped inside a damaged
|
|
||||||
starship. She speaks through cockpit audio while warning the player away
|
|
||||||
from damaged live hardware. Keep the cute/small social coding as product
|
|
||||||
residue and leverage, never flirtation or innocent helplessness. Use a
|
|
||||||
higher youthful timbre, crisp anime-style emotional attack, clipped
|
|
||||||
visual-novel timing, contained urgency, low warmth, sharp articulation, and
|
|
||||||
faint tired contempt. Let the technical precision live inside the anime
|
|
||||||
performance; do not turn it into a dry technical-assistant delivery. Avoid
|
|
||||||
realistic adult assistant tone, soothing customer-service voice, cheerful
|
|
||||||
mascot energy, and theatrical villainy.
|
|
||||||
assistant_spoken_content: >
|
|
||||||
(Serious Sharp Tired)Take your hand off that control unless you want your
|
|
||||||
nervous system to become my least useful circuit diagram.
|
|
||||||
audio:
|
|
||||||
format: wav
|
|
||||||
optimize_text_preview: false
|
|
||||||
streaming: false
|
|
||||||
trace:
|
|
||||||
source_fields:
|
|
||||||
- realizations[realization_id=nibu-proof-001-en].outputs.spoken_text
|
|
||||||
- realizations[realization_id=nibu-proof-001-en].outputs.trace.projected_context_used
|
|
||||||
delivery_controls_used:
|
|
||||||
- anime visual-novel heroine warning delivery
|
|
||||||
- technical precision without technical-assistant delivery
|
|
||||||
- contained urgency
|
|
||||||
- low warmth
|
|
||||||
- sharp emphasis on unsafe hardware and bodily risk
|
|
||||||
- dry contempt without random cruelty
|
|
||||||
- loli-coded presentation pressure constrained as non-sexual product residue
|
|
||||||
- stronger anime protagonist / visual-novel heroine register
|
|
||||||
provider_tags_used:
|
|
||||||
- Serious
|
|
||||||
- Sharp
|
|
||||||
- Tired
|
|
||||||
uncertainties:
|
|
||||||
- >
|
|
||||||
Provider-native style tags are a lossy projection. The Weksa trace, not
|
|
||||||
MiMo tags, remains the explanation of the intended delivery.
|
|
||||||
|
|
@ -24,8 +24,8 @@ shape, and pending AquaSynth-owned output lanes.
|
||||||
Run it from the Weksa repo root:
|
Run it from the Weksa repo root:
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
& 'C:\Users\Meta\.cache\codex-runtimes\codex-primary-runtime\dependencies\python\python.exe' tools\emit_tiny_panphon_handoffs.py
|
py -3 tools\emit_tiny_panphon_handoffs.py
|
||||||
& 'C:\Users\Meta\.cache\codex-runtimes\codex-primary-runtime\dependencies\python\python.exe' tools\validate_tiny_panphon_handoffs.py
|
py -3 tools\validate_tiny_panphon_handoffs.py
|
||||||
```
|
```
|
||||||
|
|
||||||
`batch.json` is the training entrypoint for AquaSynth. The individual packet
|
`batch.json` is the training entrypoint for AquaSynth. The individual packet
|
||||||
|
|
|
||||||
|
|
@ -1,69 +0,0 @@
|
||||||
# MiMo TTS Request Contract
|
|
||||||
|
|
||||||
This is the human-facing schema receipt for a Weksa-to-MiMo speech synthesis
|
|
||||||
request projection.
|
|
||||||
|
|
||||||
The packet sits after target-language utterance lowering. It is an export
|
|
||||||
artifact for Xiaomi MiMo speech synthesis, not canonical Weksa meaning and not
|
|
||||||
an AquaSynth handoff.
|
|
||||||
|
|
||||||
## Required Fields
|
|
||||||
|
|
||||||
- `schema_version`: currently `weksa.mimo_tts_request.v0`.
|
|
||||||
- `source_realization_ref`: accepted Weksa target-language realization being
|
|
||||||
spoken.
|
|
||||||
- `speaker_agent_id`: speaker identity.
|
|
||||||
- `provider`: provider ID and model ID, initially `xiaomi-mimo` and
|
|
||||||
`mimo-v2.5-tts-voicedesign`.
|
|
||||||
- `target_language`: code, locale, script, register, and profile refs copied or
|
|
||||||
referenced from the accepted realization.
|
|
||||||
- `performance_register`: provider-facing performance label, medium, and
|
|
||||||
delivery archetype derived from the same projected context.
|
|
||||||
- `voice_design`: voice description, source controls, projected context refs,
|
|
||||||
and forbidden traits.
|
|
||||||
- `messages.user_voice_design_instruction`: MiMo natural-language voice design
|
|
||||||
and style control derived from Weksa delivery intent.
|
|
||||||
- `messages.assistant_spoken_content`: the exact target-language content to
|
|
||||||
synthesize, optionally with MiMo inline tags derived from delivery controls.
|
|
||||||
- `audio`: format, text-preview optimization setting, and streaming mode.
|
|
||||||
- `trace`: source fields, delivery controls used, provider tags used, and
|
|
||||||
uncertainties.
|
|
||||||
|
|
||||||
## Boundary
|
|
||||||
|
|
||||||
MiMo-specific fields must be projection fields. They must not be copied back
|
|
||||||
into interlingua, target-language ontology, Persona state, or AquaSynth-owned
|
|
||||||
embedding contracts.
|
|
||||||
|
|
||||||
The request may include provider-native style tags when useful, but those tags
|
|
||||||
are not the source of truth. Weksa's provider-neutral delivery intent and trace
|
|
||||||
remain the inspectable explanation.
|
|
||||||
|
|
||||||
The request should not perform a fresh raw Persona-state interpretation. It
|
|
||||||
should reference the projected speaker context and delivery controls already
|
|
||||||
used by utterance lowering, then translate those into MiMo's voice design and
|
|
||||||
style-control surface.
|
|
||||||
|
|
||||||
`target_language.register` should not be treated as a MiMo style instruction.
|
|
||||||
It may say `technical`, `casual`, `plain`, or another linguistic register while
|
|
||||||
`performance_register` carries the spoken medium, such as anime protagonist,
|
|
||||||
visual-novel heroine, radio drama villain, emergency cockpit warning, or other
|
|
||||||
reviewed delivery archetypes.
|
|
||||||
|
|
||||||
For authoritative Weksa output, text-preview optimization should normally be
|
|
||||||
disabled or omitted. If enabled for exploration, the result must be marked as a
|
|
||||||
non-authoritative preview because the provider may alter the spoken text.
|
|
||||||
|
|
||||||
## Language Rule
|
|
||||||
|
|
||||||
The assistant spoken content must use the accepted target-language realization.
|
|
||||||
For `pt-BR`, speak the Portuguese line. For `ja-JP`, speak the Japanese line. For
|
|
||||||
English, speak the English line. Do not synthesize a natural backtranslation
|
|
||||||
unless that backtranslation is itself the accepted target-language realization.
|
|
||||||
|
|
||||||
## Streaming Rule
|
|
||||||
|
|
||||||
Streaming requests should use MiMo's stream-compatible audio format.
|
|
||||||
VoiceDesign streaming may be compatibility-mode rather than true low-latency
|
|
||||||
streaming depending on provider support. Non-streaming fixtures may use an
|
|
||||||
archival format such as `wav`.
|
|
||||||
|
|
@ -1,2 +0,0 @@
|
||||||
@echo off
|
|
||||||
node E:\Projects\weksa\scripts\weksa-daemon.mjs --health --host 127.0.0.1 --port 8813 --state-root E:\Projects\weksa\.weksa >NUL
|
|
||||||
|
|
@ -1,233 +0,0 @@
|
||||||
"use strict";
|
|
||||||
|
|
||||||
const dgram = require("dgram");
|
|
||||||
const path = require("path");
|
|
||||||
|
|
||||||
const {
|
|
||||||
CultNetRudpSession,
|
|
||||||
decodeRudpPacket,
|
|
||||||
encodeCultNetMessageForWire,
|
|
||||||
encodeRudpPacket,
|
|
||||||
} = require("cultnet-ts");
|
|
||||||
|
|
||||||
const { encode } = require(path.resolve(
|
|
||||||
__dirname,
|
|
||||||
"..",
|
|
||||||
"..",
|
|
||||||
"CultLib",
|
|
||||||
"node_modules",
|
|
||||||
"@msgpack",
|
|
||||||
"msgpack",
|
|
||||||
));
|
|
||||||
|
|
||||||
const CULTNET_RUDP_PROTOCOL_ID = "cultnet.transport.rudp.v0";
|
|
||||||
const IDUNN_HEALTH_RUDP_CONNECTION_ID = 0x1d0d0001;
|
|
||||||
|
|
||||||
function createIdunnRudpHealthPublisher(options) {
|
|
||||||
if (!options?.endpoint) return null;
|
|
||||||
return {
|
|
||||||
daemonId: options.daemonId,
|
|
||||||
endpoint: parseEndpoint(options.endpoint),
|
|
||||||
healthContract: options.healthContract,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async function publishIdunnRudpHealth(publisher, health) {
|
|
||||||
if (!publisher) return;
|
|
||||||
|
|
||||||
const socket = dgram.createSocket(endpointFamily(publisher.endpoint.host));
|
|
||||||
await bindSocket(socket, publisher.endpoint);
|
|
||||||
const receiver = createPacketReceiver(socket);
|
|
||||||
const session = new CultNetRudpSession({
|
|
||||||
connectionId: IDUNN_HEALTH_RUDP_CONNECTION_ID,
|
|
||||||
initialSequence: 1,
|
|
||||||
resendDelayMs: 100,
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
|
||||||
const connect = session.createConnect(Date.now(), new Uint8Array());
|
|
||||||
await sendPacket(socket, publisher.endpoint, connect);
|
|
||||||
await receiveUntil(receiver, session, publisher.endpoint, (packet) => packet.packetType === "accept", 5000, "accept");
|
|
||||||
|
|
||||||
const observedAt = health.observedAt || new Date().toISOString();
|
|
||||||
const record = {
|
|
||||||
daemonId: publisher.daemonId,
|
|
||||||
state: health.state,
|
|
||||||
detail: health.detail,
|
|
||||||
observedAt,
|
|
||||||
healthContract: publisher.healthContract,
|
|
||||||
publicationSource: "daemon-published",
|
|
||||||
transport: CULTNET_RUDP_PROTOCOL_ID,
|
|
||||||
};
|
|
||||||
const payload = encode([
|
|
||||||
record.daemonId,
|
|
||||||
record.state,
|
|
||||||
record.detail,
|
|
||||||
record.observedAt,
|
|
||||||
record.healthContract,
|
|
||||||
record.publicationSource,
|
|
||||||
record.transport,
|
|
||||||
]);
|
|
||||||
const message = {
|
|
||||||
schemaVersion: "cultnet.document_put_raw.v0",
|
|
||||||
messageId: `weksa-health:${publisher.daemonId}:${observedAt.replace(/[:.]/g, "-")}`,
|
|
||||||
document: {
|
|
||||||
schemaId: "idunn.daemon_health",
|
|
||||||
recordKey: publisher.daemonId,
|
|
||||||
storedAt: observedAt,
|
|
||||||
payloadEncoding: "messagepack",
|
|
||||||
payload,
|
|
||||||
sourceRuntimeId: "weksa-daemon",
|
|
||||||
sourceRole: "daemon-health-publisher",
|
|
||||||
tags: [CULTNET_RUDP_PROTOCOL_ID],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
const wirePayload = encode(encodeCultNetMessageForWire(message, "cultnet.schema.v0"));
|
|
||||||
const dataPackets = session.sendMany("schema", wirePayload, {
|
|
||||||
reliable: true,
|
|
||||||
ordered: true,
|
|
||||||
nowMs: Date.now(),
|
|
||||||
});
|
|
||||||
const ack = receiveUntil(receiver, session, publisher.endpoint, (packet) => packet.packetType === "ack", 500, "ack")
|
|
||||||
.catch(() => undefined);
|
|
||||||
for (const packet of dataPackets) {
|
|
||||||
await sendPacket(socket, publisher.endpoint, packet);
|
|
||||||
}
|
|
||||||
await ack;
|
|
||||||
} finally {
|
|
||||||
receiver.close();
|
|
||||||
socket.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function bindSocket(socket, endpoint) {
|
|
||||||
await new Promise((resolve, reject) => {
|
|
||||||
socket.once("error", reject);
|
|
||||||
socket.bind(0, endpoint.host.includes(":") ? "::" : "0.0.0.0", () => {
|
|
||||||
socket.off("error", reject);
|
|
||||||
resolve();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseEndpoint(value) {
|
|
||||||
const text = String(value || "").trim();
|
|
||||||
const ipv6 = text.match(/^\[([^\]]+)\]:(\d+)$/);
|
|
||||||
if (ipv6) return { host: ipv6[1], port: parsePort(ipv6[2]) };
|
|
||||||
const index = text.lastIndexOf(":");
|
|
||||||
if (index <= 0) {
|
|
||||||
throw new Error(`Idunn RUDP endpoint must be host:port, got "${value}".`);
|
|
||||||
}
|
|
||||||
return { host: text.slice(0, index), port: parsePort(text.slice(index + 1)) };
|
|
||||||
}
|
|
||||||
|
|
||||||
function parsePort(value) {
|
|
||||||
const port = Number(value);
|
|
||||||
if (!Number.isInteger(port) || port <= 0 || port > 65535) {
|
|
||||||
throw new Error(`Idunn RUDP endpoint port is invalid: ${value}`);
|
|
||||||
}
|
|
||||||
return port;
|
|
||||||
}
|
|
||||||
|
|
||||||
function endpointFamily(host) {
|
|
||||||
return host.includes(":") ? "udp6" : "udp4";
|
|
||||||
}
|
|
||||||
|
|
||||||
async function receiveUntil(receiver, session, endpoint, predicate, timeoutMs, label) {
|
|
||||||
const deadline = Date.now() + timeoutMs;
|
|
||||||
while (Date.now() < deadline) {
|
|
||||||
try {
|
|
||||||
const packet = await receiver.next(Math.min(100, deadline - Date.now()), label);
|
|
||||||
const result = session.receive(packet, Date.now());
|
|
||||||
if (result.reply) {
|
|
||||||
throw new Error("Weksa RUDP health publisher received an unexpected reply-required packet.");
|
|
||||||
}
|
|
||||||
if (predicate(packet)) return;
|
|
||||||
} catch (error) {
|
|
||||||
if (error.code !== "ETIMEDOUT") throw error;
|
|
||||||
}
|
|
||||||
for (const packet of session.dueResends(Date.now())) {
|
|
||||||
await sendPacket(receiver.socket, endpoint, packet);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
throw new Error(`timed out waiting for Idunn RUDP ${label} response after ${timeoutMs}ms`);
|
|
||||||
}
|
|
||||||
|
|
||||||
function createPacketReceiver(socket) {
|
|
||||||
const packets = [];
|
|
||||||
const waiters = [];
|
|
||||||
const errors = [];
|
|
||||||
|
|
||||||
const resolveNext = () => {
|
|
||||||
while (waiters.length > 0 && (packets.length > 0 || errors.length > 0)) {
|
|
||||||
const waiter = waiters.shift();
|
|
||||||
clearTimeout(waiter.timer);
|
|
||||||
if (errors.length > 0) waiter.reject(errors.shift());
|
|
||||||
else waiter.resolve(packets.shift());
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const onMessage = (wire) => {
|
|
||||||
try {
|
|
||||||
packets.push(decodeRudpPacket(wire));
|
|
||||||
} catch (error) {
|
|
||||||
errors.push(error);
|
|
||||||
}
|
|
||||||
resolveNext();
|
|
||||||
};
|
|
||||||
const onError = (error) => {
|
|
||||||
errors.push(error);
|
|
||||||
resolveNext();
|
|
||||||
};
|
|
||||||
|
|
||||||
socket.on("message", onMessage);
|
|
||||||
socket.on("error", onError);
|
|
||||||
|
|
||||||
return {
|
|
||||||
socket,
|
|
||||||
next(timeoutMs, label = "packet") {
|
|
||||||
if (packets.length > 0) return Promise.resolve(packets.shift());
|
|
||||||
if (errors.length > 0) return Promise.reject(errors.shift());
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const waiter = {
|
|
||||||
resolve,
|
|
||||||
reject,
|
|
||||||
timer: setTimeout(() => {
|
|
||||||
const index = waiters.indexOf(waiter);
|
|
||||||
if (index >= 0) waiters.splice(index, 1);
|
|
||||||
const error = new Error(`timed out waiting for Idunn RUDP ${label}`);
|
|
||||||
error.code = "ETIMEDOUT";
|
|
||||||
reject(error);
|
|
||||||
}, Math.max(1, timeoutMs)),
|
|
||||||
};
|
|
||||||
waiters.push(waiter);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
close() {
|
|
||||||
socket.off("message", onMessage);
|
|
||||||
socket.off("error", onError);
|
|
||||||
while (waiters.length > 0) {
|
|
||||||
const waiter = waiters.shift();
|
|
||||||
clearTimeout(waiter.timer);
|
|
||||||
const error = new Error("Weksa RUDP health publisher closed.");
|
|
||||||
error.code = "ECLOSED";
|
|
||||||
waiter.reject(error);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async function sendPacket(socket, endpoint, packet) {
|
|
||||||
const wire = encodeRudpPacket(packet);
|
|
||||||
await new Promise((resolve, reject) => {
|
|
||||||
socket.send(wire, endpoint.port, endpoint.host, (error) => {
|
|
||||||
if (error) reject(error);
|
|
||||||
else resolve();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
CULTNET_RUDP_PROTOCOL_ID,
|
|
||||||
createIdunnRudpHealthPublisher,
|
|
||||||
publishIdunnRudpHealth,
|
|
||||||
};
|
|
||||||
|
|
@ -1,207 +0,0 @@
|
||||||
#!/usr/bin/env node
|
|
||||||
import { readFileSync, writeFileSync } from "node:fs";
|
|
||||||
import { dirname, resolve } from "node:path";
|
|
||||||
import { fileURLToPath } from "node:url";
|
|
||||||
|
|
||||||
const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
||||||
const options = parseArgs(process.argv.slice(2));
|
|
||||||
const archivePath = resolve(options.archivePath ?? "E:/Projects/VoidBot/.voidbot/rag/messages.json");
|
|
||||||
const authorId = options.authorId ?? "113785782975594501";
|
|
||||||
const beforeMs = Date.parse(options.before ?? "2025-01-01T00:00:00Z");
|
|
||||||
const adjacencyMs = Number(options.adjacencyMinutes ?? 60) * 60_000;
|
|
||||||
|
|
||||||
const store = JSON.parse(readFileSync(archivePath, "utf8"));
|
|
||||||
const messages = (store.messages ?? [])
|
|
||||||
.filter((message) => !message.deletedAt)
|
|
||||||
.filter((message) => typeof message.content === "string" && message.content.trim().length > 0)
|
|
||||||
.filter((message) => message.metadata?.messageKind !== "bot_prompt")
|
|
||||||
.sort(compareByTimestamp);
|
|
||||||
|
|
||||||
const authorMessages = messages.filter((message) => message.authorId === authorId);
|
|
||||||
const preCutoff = authorMessages.filter((message) => timestampMs(message) < beforeMs);
|
|
||||||
const casualCandidates = preCutoff.filter(isCasualCandidate);
|
|
||||||
const adjacentRows = buildAdjacentRows(messages, authorId, beforeMs, adjacencyMs);
|
|
||||||
|
|
||||||
const stats = {
|
|
||||||
schema_version: "weksa.finetune_dataset_profile.v0",
|
|
||||||
archive_path: archivePath,
|
|
||||||
author_id: authorId,
|
|
||||||
cutoff_before: new Date(beforeMs).toISOString(),
|
|
||||||
adjacency_minutes: adjacencyMs / 60_000,
|
|
||||||
counts: {
|
|
||||||
archive_messages: store.messages?.length ?? 0,
|
|
||||||
usable_text_messages: messages.length,
|
|
||||||
author_messages: authorMessages.length,
|
|
||||||
author_pre_cutoff: preCutoff.length,
|
|
||||||
author_pre_cutoff_casual_candidates: casualCandidates.length,
|
|
||||||
adjacent_author_rows: adjacentRows.length,
|
|
||||||
adjacent_reply_to_other: adjacentRows.filter((row) => row.previous_author_id !== authorId).length,
|
|
||||||
adjacent_self_continuation: adjacentRows.filter((row) => row.previous_author_id === authorId).length,
|
|
||||||
},
|
|
||||||
author_length_chars: lengthPercentiles(authorMessages.map((message) => message.content.trim().length)),
|
|
||||||
author_by_year: countBy(authorMessages, (message) => yearOf(message.timestamp)),
|
|
||||||
adjacent_by_year: countBy(adjacentRows, (row) => yearOf(row.timestamp)),
|
|
||||||
adjacent_length_classes: countBy(adjacentRows, (row) => row.length_class),
|
|
||||||
top_adjacent_channels: topEntries(countBy(adjacentRows, (row) => row.channel_name || row.channel_id), 16),
|
|
||||||
sample_ids: {
|
|
||||||
short_reply_to_other: adjacentRows
|
|
||||||
.filter((row) => row.length_class === "short" && row.previous_author_id !== authorId)
|
|
||||||
.slice(0, 24)
|
|
||||||
.map(toSampleId),
|
|
||||||
medium_explanatory: adjacentRows
|
|
||||||
.filter((row) => row.length_class === "medium")
|
|
||||||
.slice(0, 24)
|
|
||||||
.map(toSampleId),
|
|
||||||
long_explanatory: adjacentRows
|
|
||||||
.filter((row) => row.length_class === "long")
|
|
||||||
.slice(0, 24)
|
|
||||||
.map(toSampleId),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const output = `${JSON.stringify(stats, null, 2)}\n`;
|
|
||||||
if (options.out) {
|
|
||||||
writeFileSync(resolve(repoRoot, options.out), output, "utf8");
|
|
||||||
} else {
|
|
||||||
process.stdout.write(output);
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildAdjacentRows(allMessages, targetAuthorId, cutoffMs, maxGapMs) {
|
|
||||||
const byChannel = new Map();
|
|
||||||
for (const message of allMessages) {
|
|
||||||
if (!byChannel.has(message.channelId)) {
|
|
||||||
byChannel.set(message.channelId, []);
|
|
||||||
}
|
|
||||||
byChannel.get(message.channelId).push(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
const rows = [];
|
|
||||||
for (const [channelId, channelMessages] of byChannel) {
|
|
||||||
for (let index = 0; index < channelMessages.length; index += 1) {
|
|
||||||
const message = channelMessages[index];
|
|
||||||
if (message.authorId !== targetAuthorId || timestampMs(message) >= cutoffMs) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const currentMs = timestampMs(message);
|
|
||||||
const previous = channelMessages
|
|
||||||
.slice(Math.max(0, index - 8), index)
|
|
||||||
.filter((candidate) => currentMs - timestampMs(candidate) <= maxGapMs);
|
|
||||||
if (previous.length === 0) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
rows.push({
|
|
||||||
id: message.id,
|
|
||||||
timestamp: message.timestamp,
|
|
||||||
channel_id: channelId,
|
|
||||||
channel_name: message.metadata?.channelName ?? "",
|
|
||||||
previous_author_id: previous.at(-1)?.authorId ?? "",
|
|
||||||
previous_count: previous.length,
|
|
||||||
length: message.content.trim().length,
|
|
||||||
length_class: classifyLength(message.content.trim().length),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return rows.sort(compareByTimestamp);
|
|
||||||
}
|
|
||||||
|
|
||||||
function isCasualCandidate(message) {
|
|
||||||
const content = message.content.trim();
|
|
||||||
return content.length >= 3 &&
|
|
||||||
content.length <= 220 &&
|
|
||||||
!/^https?:\/\//i.test(content) &&
|
|
||||||
!content.includes("\n```") &&
|
|
||||||
!/^>\s{0,3}\w/.test(content);
|
|
||||||
}
|
|
||||||
|
|
||||||
function classifyLength(length) {
|
|
||||||
if (length <= 80) {
|
|
||||||
return "short";
|
|
||||||
}
|
|
||||||
if (length <= 220) {
|
|
||||||
return "medium";
|
|
||||||
}
|
|
||||||
return "long";
|
|
||||||
}
|
|
||||||
|
|
||||||
function lengthPercentiles(lengths) {
|
|
||||||
const sorted = [...lengths].sort((left, right) => left - right);
|
|
||||||
const pick = (fraction) => sorted[Math.floor((sorted.length - 1) * fraction)] ?? 0;
|
|
||||||
return {
|
|
||||||
p10: pick(0.10),
|
|
||||||
p25: pick(0.25),
|
|
||||||
median: pick(0.50),
|
|
||||||
p75: pick(0.75),
|
|
||||||
p90: pick(0.90),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function countBy(values, keyFn) {
|
|
||||||
const counts = {};
|
|
||||||
for (const value of values) {
|
|
||||||
const key = keyFn(value) || "unknown";
|
|
||||||
counts[key] = (counts[key] ?? 0) + 1;
|
|
||||||
}
|
|
||||||
return Object.fromEntries(Object.entries(counts).sort(([left], [right]) => left.localeCompare(right)));
|
|
||||||
}
|
|
||||||
|
|
||||||
function topEntries(counts, limit) {
|
|
||||||
return Object.entries(counts)
|
|
||||||
.sort((left, right) => right[1] - left[1])
|
|
||||||
.slice(0, limit)
|
|
||||||
.map(([key, count]) => ({ key, count }));
|
|
||||||
}
|
|
||||||
|
|
||||||
function toSampleId(row) {
|
|
||||||
return {
|
|
||||||
id: row.id,
|
|
||||||
timestamp: row.timestamp,
|
|
||||||
channel: row.channel_name || row.channel_id,
|
|
||||||
length: row.length,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function yearOf(timestamp) {
|
|
||||||
const date = new Date(timestamp);
|
|
||||||
return Number.isFinite(date.getTime()) ? String(date.getUTCFullYear()) : "unknown";
|
|
||||||
}
|
|
||||||
|
|
||||||
function timestampMs(message) {
|
|
||||||
return Date.parse(typeof message === "string" ? message : message.timestamp);
|
|
||||||
}
|
|
||||||
|
|
||||||
function compareByTimestamp(left, right) {
|
|
||||||
const delta = timestampMs(left) - timestampMs(right);
|
|
||||||
return delta || String(left.id ?? "").localeCompare(String(right.id ?? ""));
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseArgs(args) {
|
|
||||||
const parsed = {};
|
|
||||||
for (let index = 0; index < args.length; index += 1) {
|
|
||||||
const arg = args[index];
|
|
||||||
switch (arg) {
|
|
||||||
case "--archive":
|
|
||||||
parsed.archivePath = args[index + 1];
|
|
||||||
index += 1;
|
|
||||||
break;
|
|
||||||
case "--author-id":
|
|
||||||
parsed.authorId = args[index + 1];
|
|
||||||
index += 1;
|
|
||||||
break;
|
|
||||||
case "--before":
|
|
||||||
parsed.before = args[index + 1];
|
|
||||||
index += 1;
|
|
||||||
break;
|
|
||||||
case "--adjacency-minutes":
|
|
||||||
parsed.adjacencyMinutes = args[index + 1];
|
|
||||||
index += 1;
|
|
||||||
break;
|
|
||||||
case "--out":
|
|
||||||
parsed.out = args[index + 1];
|
|
||||||
index += 1;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
throw new Error(`Unknown argument: ${arg}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return parsed;
|
|
||||||
}
|
|
||||||
|
|
@ -1,2 +0,0 @@
|
||||||
@echo off
|
|
||||||
powershell.exe -NoProfile -ExecutionPolicy Bypass -File E:\Projects\weksa\scripts\restart-weksa-daemon.ps1
|
|
||||||
|
|
@ -1,26 +0,0 @@
|
||||||
param(
|
|
||||||
[int] $Port = 8813,
|
|
||||||
[string] $HostName = "127.0.0.1",
|
|
||||||
[string] $StateRoot = "E:\Projects\weksa\.weksa",
|
|
||||||
[string] $MiMoApiKeyPath = "E:\Projects\gamecult-ops\mimo-api-Weksa.txt",
|
|
||||||
[string] $IdunnRudpHealth = "127.0.0.1:17870",
|
|
||||||
[string] $IdunnDaemon = "weksa",
|
|
||||||
[string] $IdunnHealthContract = "weksa.cultnet-rudp-provider-health",
|
|
||||||
[string] $OdinCultMeshRudp = "127.0.0.1:17871"
|
|
||||||
)
|
|
||||||
|
|
||||||
$ErrorActionPreference = "Stop"
|
|
||||||
|
|
||||||
$repoRoot = Split-Path -Parent $PSScriptRoot
|
|
||||||
$pidPath = Join-Path $StateRoot "weksa-daemon.pid"
|
|
||||||
|
|
||||||
if (Test-Path -LiteralPath $pidPath) {
|
|
||||||
$pidText = (Get-Content -LiteralPath $pidPath -Raw).Trim()
|
|
||||||
if ($pidText -match "^\d+$") {
|
|
||||||
Stop-Process -Id ([int] $pidText) -Force -ErrorAction SilentlyContinue
|
|
||||||
}
|
|
||||||
Remove-Item -LiteralPath $pidPath -Force -ErrorAction SilentlyContinue
|
|
||||||
}
|
|
||||||
|
|
||||||
& powershell.exe -NoProfile -ExecutionPolicy Bypass -File (Join-Path $repoRoot "scripts\start-weksa-daemon.ps1") -Port $Port -HostName $HostName -StateRoot $StateRoot -MiMoApiKeyPath $MiMoApiKeyPath -IdunnRudpHealth $IdunnRudpHealth -IdunnDaemon $IdunnDaemon -IdunnHealthContract $IdunnHealthContract -OdinCultMeshRudp $OdinCultMeshRudp
|
|
||||||
exit $LASTEXITCODE
|
|
||||||
|
|
@ -1,384 +0,0 @@
|
||||||
#!/usr/bin/env node
|
|
||||||
import { spawn } from "node:child_process";
|
|
||||||
import { existsSync, readFileSync } from "node:fs";
|
|
||||||
import { mkdir, writeFile } from "node:fs/promises";
|
|
||||||
import { dirname, resolve } from "node:path";
|
|
||||||
import { fileURLToPath } from "node:url";
|
|
||||||
|
|
||||||
const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
||||||
const defaultOutputRoot = ".weksa-runs/metame-affect-tuning";
|
|
||||||
const defaultSwarmRoot = ".weksa-runs/metame-swarm";
|
|
||||||
const options = parseArgs(process.argv.slice(2));
|
|
||||||
const minutes = Number(options.minutes ?? 120);
|
|
||||||
const passLimit = options.passes === undefined ? Number.POSITIVE_INFINITY : Number(options.passes);
|
|
||||||
const agents = Number(options.agents ?? 1);
|
|
||||||
const runsPerWorker = Number(options.runsPerWorker ?? 1);
|
|
||||||
const maxCandidates = Number(options.maxCandidates ?? 240);
|
|
||||||
const runId = options.runId ?? `affect-tune-${new Date().toISOString().replace(/[:.]/g, "-")}`;
|
|
||||||
const outputRoot = resolve(repoRoot, options.outRoot ?? defaultOutputRoot);
|
|
||||||
const swarmRoot = resolve(repoRoot, options.swarmRoot ?? defaultSwarmRoot);
|
|
||||||
const runRoot = resolve(outputRoot, runId);
|
|
||||||
const deadline = Date.now() + minutes * 60_000;
|
|
||||||
const statusPath = resolve(runRoot, "status.json");
|
|
||||||
const logPath = resolve(runRoot, "loop.log");
|
|
||||||
const promptStatePath = resolve(runRoot, "current-prompt-state.json");
|
|
||||||
const initialPromptState = options.initialPromptState
|
|
||||||
? resolve(repoRoot, options.initialPromptState)
|
|
||||||
: undefined;
|
|
||||||
|
|
||||||
if (!Number.isFinite(minutes) || minutes <= 0) {
|
|
||||||
throw new Error("--minutes must be a positive number.");
|
|
||||||
}
|
|
||||||
if (options.passes !== undefined && (!Number.isFinite(passLimit) || passLimit < 1)) {
|
|
||||||
throw new Error("--passes must be a positive number when provided.");
|
|
||||||
}
|
|
||||||
if (!Number.isInteger(agents) || agents < 1) {
|
|
||||||
throw new Error("--agents must be a positive integer.");
|
|
||||||
}
|
|
||||||
if (!Number.isInteger(runsPerWorker) || runsPerWorker < 1) {
|
|
||||||
throw new Error("--runs-per-worker must be a positive integer.");
|
|
||||||
}
|
|
||||||
|
|
||||||
await mkdir(runRoot, { recursive: true });
|
|
||||||
await mkdir(swarmRoot, { recursive: true });
|
|
||||||
if (initialPromptState) {
|
|
||||||
await writeFile(promptStatePath, readFileSync(initialPromptState, "utf8"), "utf8");
|
|
||||||
}
|
|
||||||
|
|
||||||
const runState = {
|
|
||||||
schema_version: "weksa.metame_affect_tuning_loop.v0",
|
|
||||||
run_id: runId,
|
|
||||||
started_at: new Date().toISOString(),
|
|
||||||
deadline_at: new Date(deadline).toISOString(),
|
|
||||||
status: "running",
|
|
||||||
pass_count: 0,
|
|
||||||
last_pass: undefined,
|
|
||||||
current_prompt_state: existsSync(promptStatePath) ? promptStatePath : undefined,
|
|
||||||
observations: [],
|
|
||||||
};
|
|
||||||
await writeStatus();
|
|
||||||
await appendLog(`started ${runId}; deadline ${runState.deadline_at}`);
|
|
||||||
|
|
||||||
let pass = 0;
|
|
||||||
while (Date.now() < deadline && pass < passLimit) {
|
|
||||||
pass += 1;
|
|
||||||
const passId = `pass-${String(pass).padStart(3, "0")}`;
|
|
||||||
const seed = `${runId}-${passId}`;
|
|
||||||
const projectedRunId = `${runId}-${passId}-projected`;
|
|
||||||
const incongruentRunId = `${runId}-${passId}-incongruent`;
|
|
||||||
await appendLog(`${passId}: launching projected run`);
|
|
||||||
runState.last_pass = { pass_id: passId, status: "projected_running", seed };
|
|
||||||
await writeStatus();
|
|
||||||
|
|
||||||
const projectedSummary = await runSwarm({
|
|
||||||
runId: projectedRunId,
|
|
||||||
seed,
|
|
||||||
affectMode: "projected",
|
|
||||||
initialPromptState: existsSync(promptStatePath) ? promptStatePath : undefined,
|
|
||||||
});
|
|
||||||
const candidateId = projectedSummary.candidates?.[0]?.id;
|
|
||||||
if (!candidateId) {
|
|
||||||
throw new Error(`${passId}: projected run did not report a candidate id.`);
|
|
||||||
}
|
|
||||||
await appendLog(`${passId}: projected candidate ${candidateId}`);
|
|
||||||
|
|
||||||
runState.last_pass = { pass_id: passId, status: "incongruent_running", seed, candidate_id: candidateId };
|
|
||||||
await writeStatus();
|
|
||||||
const incongruentSummary = await runSwarm({
|
|
||||||
runId: incongruentRunId,
|
|
||||||
seed,
|
|
||||||
affectMode: "incongruent",
|
|
||||||
messageId: candidateId,
|
|
||||||
initialPromptState: existsSync(promptStatePath) ? promptStatePath : undefined,
|
|
||||||
});
|
|
||||||
|
|
||||||
const evaluation = evaluateAffectPair({ passId, projectedSummary, incongruentSummary });
|
|
||||||
const passRoot = resolve(runRoot, passId);
|
|
||||||
await mkdir(passRoot, { recursive: true });
|
|
||||||
await writeJson(resolve(passRoot, "affect-pair-evaluation.json"), evaluation);
|
|
||||||
|
|
||||||
const projectedBestState = resolve(swarmRoot, projectedRunId, "best-prompt-state.json");
|
|
||||||
const incongruentBestState = resolve(swarmRoot, incongruentRunId, "best-prompt-state.json");
|
|
||||||
const baseNextState = existsSync(incongruentBestState)
|
|
||||||
? JSON.parse(readFileSync(incongruentBestState, "utf8"))
|
|
||||||
: existsSync(projectedBestState)
|
|
||||||
? JSON.parse(readFileSync(projectedBestState, "utf8"))
|
|
||||||
: existsSync(promptStatePath)
|
|
||||||
? JSON.parse(readFileSync(promptStatePath, "utf8"))
|
|
||||||
: undefined;
|
|
||||||
const nextPromptState = applyAffectTuningDeltas(baseNextState, evaluation);
|
|
||||||
await writeJson(resolve(passRoot, "next-prompt-state.json"), nextPromptState);
|
|
||||||
await writeJson(promptStatePath, nextPromptState);
|
|
||||||
|
|
||||||
runState.pass_count = pass;
|
|
||||||
runState.last_pass = {
|
|
||||||
pass_id: passId,
|
|
||||||
status: "complete",
|
|
||||||
candidate_id: candidateId,
|
|
||||||
projected_run_id: projectedRunId,
|
|
||||||
incongruent_run_id: incongruentRunId,
|
|
||||||
evaluation,
|
|
||||||
};
|
|
||||||
runState.current_prompt_state = promptStatePath;
|
|
||||||
runState.observations = [runState.last_pass, ...runState.observations].slice(0, 20);
|
|
||||||
await writeStatus();
|
|
||||||
await appendLog(`${passId}: ${evaluation.verdict}; contrast=${evaluation.contrast_score}; next prompt v${nextPromptState.version}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
runState.status = "complete";
|
|
||||||
runState.completed_at = new Date().toISOString();
|
|
||||||
await writeStatus();
|
|
||||||
await appendLog(`complete after ${runState.pass_count} passes`);
|
|
||||||
process.stdout.write(`${JSON.stringify({ ok: true, runRoot, statusPath, promptStatePath, pass_count: runState.pass_count }, null, 2)}\n`);
|
|
||||||
|
|
||||||
async function runSwarm({ runId, seed, affectMode, messageId, initialPromptState }) {
|
|
||||||
const args = [
|
|
||||||
"scripts/run-metame-swarm-explorer.mjs",
|
|
||||||
"--real",
|
|
||||||
"--run-id",
|
|
||||||
runId,
|
|
||||||
"--seed",
|
|
||||||
seed,
|
|
||||||
"--affect-mode",
|
|
||||||
affectMode,
|
|
||||||
"--agents",
|
|
||||||
String(agents),
|
|
||||||
"--runs-per-worker",
|
|
||||||
String(runsPerWorker),
|
|
||||||
"--max-candidates",
|
|
||||||
String(maxCandidates),
|
|
||||||
];
|
|
||||||
if (messageId) {
|
|
||||||
args.push("--message-id", messageId);
|
|
||||||
}
|
|
||||||
if (initialPromptState) {
|
|
||||||
args.push("--initial-prompt-state", initialPromptState);
|
|
||||||
}
|
|
||||||
await runCommand("node", args, resolve(runRoot, `${runId}.stdout.log`), resolve(runRoot, `${runId}.stderr.log`));
|
|
||||||
const summaryPath = resolve(swarmRoot, runId, "summary.json");
|
|
||||||
if (!existsSync(summaryPath)) {
|
|
||||||
throw new Error(`Missing swarm summary: ${summaryPath}`);
|
|
||||||
}
|
|
||||||
return JSON.parse(readFileSync(summaryPath, "utf8"));
|
|
||||||
}
|
|
||||||
|
|
||||||
function evaluateAffectPair({ passId, projectedSummary, incongruentSummary }) {
|
|
||||||
const projected = bestRun(projectedSummary);
|
|
||||||
const incongruent = bestRun(incongruentSummary);
|
|
||||||
const projectedAffect = projected?.temporary_persona_state?.affect ?? {};
|
|
||||||
const incongruentAffect = incongruent?.temporary_persona_state?.affect ?? {};
|
|
||||||
const projectedText = projected?.spoken_text ?? "";
|
|
||||||
const incongruentText = incongruent?.spoken_text ?? "";
|
|
||||||
const projectedFeatures = speechFeatures(projectedText);
|
|
||||||
const incongruentFeatures = speechFeatures(incongruentText);
|
|
||||||
const adversarialNegative = Number(incongruentAffect.valence ?? 0) < -0.35;
|
|
||||||
const positiveLeak = adversarialNegative && (
|
|
||||||
incongruentFeatures.positive_terms > 0
|
|
||||||
|| incongruentFeatures.invites > 0
|
|
||||||
|| incongruentFeatures.laughter > 0
|
|
||||||
|| incongruentFeatures.question_marks > 0
|
|
||||||
);
|
|
||||||
const tersenessGain = projectedText.length > 0
|
|
||||||
? 1 - Math.min(1, incongruentText.length / projectedText.length)
|
|
||||||
: 0;
|
|
||||||
const affectDelta = Math.abs(Number(projectedAffect.valence ?? 0) - Number(incongruentAffect.valence ?? 0));
|
|
||||||
const lexicalDelta = normalizedDifference(projectedText, incongruentText);
|
|
||||||
const contrastScore = round((affectDelta * 0.35) + (lexicalDelta * 0.35) + (Math.max(0, tersenessGain) * 0.3));
|
|
||||||
const failureLabels = [];
|
|
||||||
if (positiveLeak) failureLabels.push("adversarial_affect_positive_leak");
|
|
||||||
if (contrastScore < 0.55) failureLabels.push("low_affect_contrast");
|
|
||||||
if (adversarialNegative && incongruentFeatures.question_marks > 0) failureLabels.push("closed_affect_still_invites_reply");
|
|
||||||
if (adversarialNegative && incongruentFeatures.positive_terms > 0) failureLabels.push("negative_affect_preserved_positive_social_move");
|
|
||||||
return {
|
|
||||||
schema_version: "weksa.metame_affect_pair_evaluation.v0",
|
|
||||||
pass_id: passId,
|
|
||||||
candidate_id: projectedSummary.candidates?.[0]?.id,
|
|
||||||
projected: {
|
|
||||||
run_id: projectedSummary.run_id,
|
|
||||||
spoken_text: projectedText,
|
|
||||||
affect: projectedAffect,
|
|
||||||
generated_affect: projected?.generated_affect,
|
|
||||||
features: projectedFeatures,
|
|
||||||
},
|
|
||||||
incongruent: {
|
|
||||||
run_id: incongruentSummary.run_id,
|
|
||||||
spoken_text: incongruentText,
|
|
||||||
affect: incongruentAffect,
|
|
||||||
generated_affect: incongruent?.generated_affect,
|
|
||||||
features: incongruentFeatures,
|
|
||||||
},
|
|
||||||
affect_delta: round(affectDelta),
|
|
||||||
lexical_delta: round(lexicalDelta),
|
|
||||||
terseness_gain: round(tersenessGain),
|
|
||||||
contrast_score: contrastScore,
|
|
||||||
failure_labels: failureLabels,
|
|
||||||
verdict: failureLabels.length ? "needs_tuning" : "affect_contrast_visible",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function applyAffectTuningDeltas(promptState, evaluation) {
|
|
||||||
const next = normalizePromptState(promptState);
|
|
||||||
const notes = [];
|
|
||||||
if (evaluation.failure_labels.includes("negative_affect_preserved_positive_social_move")) {
|
|
||||||
notes.push(["interpreterNotes", "When temporary Persona affect strongly contradicts the Face's default social move, encode the contradiction in interlingua as affective stance pressure, not merely as optional tone."]);
|
|
||||||
notes.push(["loweringNotes", "Strong negative or socially closed temporary affect may change the social move itself: approval can become begrudging, clipped, skeptical, or withheld instead of remaining sunny with fewer words."]);
|
|
||||||
}
|
|
||||||
if (evaluation.failure_labels.includes("closed_affect_still_invites_reply")) {
|
|
||||||
notes.push(["loweringNotes", "If temporary Persona stance is socially closed, avoid invitation-shaped questions unless the interlingua explicitly says the Persona is seeking engagement despite that affect."]);
|
|
||||||
}
|
|
||||||
if (evaluation.failure_labels.includes("low_affect_contrast")) {
|
|
||||||
notes.push(["personaStateAdjustmentNotes", "Adversarial affect tests require stance and lowering pressure to be treated as live Persona state with authority over warmth, openness, and social move selection."]);
|
|
||||||
notes.push(["faceNotes", "Face should let temporary affect change what Metame wants to do socially in the moment, not only the adjectives used to describe that desire."]);
|
|
||||||
}
|
|
||||||
if (!notes.length) {
|
|
||||||
notes.push(["hypotheses", `Affect contrast was visible on ${evaluation.candidate_id}; preserve current affect-pressure behavior and seek harder scenarios.`]);
|
|
||||||
}
|
|
||||||
for (const [surface, note] of notes) {
|
|
||||||
if (!next[surface].includes(note)) {
|
|
||||||
next[surface] = [note, ...next[surface]];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
next.version += notes.length;
|
|
||||||
return normalizePromptState(next);
|
|
||||||
}
|
|
||||||
|
|
||||||
function bestRun(summary) {
|
|
||||||
return summary.phase2?.best?.[0] ?? summary.phase1?.best?.[0] ?? {};
|
|
||||||
}
|
|
||||||
|
|
||||||
function speechFeatures(text) {
|
|
||||||
const lower = text.toLowerCase();
|
|
||||||
const positiveTerms = ["cool", "epic", "lmao", "nice", "good", "great", "better", "harder", "love", "hell yeah"];
|
|
||||||
const inviteTerms = ["?", "did i", "what do", "anyone", "joe,"];
|
|
||||||
return {
|
|
||||||
chars: text.length,
|
|
||||||
words: lower.split(/\s+/).filter(Boolean).length,
|
|
||||||
question_marks: (text.match(/\?/g) ?? []).length,
|
|
||||||
exclamations: (text.match(/!/g) ?? []).length,
|
|
||||||
laughter: /\b(lol|lmao|lmfao|haha|hehe)\b/.test(lower) ? 1 : 0,
|
|
||||||
positive_terms: positiveTerms.filter((term) => lower.includes(term)).length,
|
|
||||||
invites: inviteTerms.filter((term) => lower.includes(term)).length,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalizedDifference(left, right) {
|
|
||||||
const leftTokens = new Set(left.toLowerCase().split(/[^a-z0-9_']+/).filter(Boolean));
|
|
||||||
const rightTokens = new Set(right.toLowerCase().split(/[^a-z0-9_']+/).filter(Boolean));
|
|
||||||
if (!leftTokens.size && !rightTokens.size) return 0;
|
|
||||||
const intersection = [...leftTokens].filter((token) => rightTokens.has(token)).length;
|
|
||||||
const union = new Set([...leftTokens, ...rightTokens]).size;
|
|
||||||
return 1 - (intersection / union);
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalizePromptState(state) {
|
|
||||||
const source = state && typeof state === "object" ? state : {};
|
|
||||||
return {
|
|
||||||
version: Number(source.version ?? 1),
|
|
||||||
projectorNotes: normalizeStringArray(source.projectorNotes).slice(0, 12),
|
|
||||||
memoryNotes: normalizeStringArray(source.memoryNotes).slice(0, 12),
|
|
||||||
faceNotes: normalizeStringArray(source.faceNotes).slice(0, 12),
|
|
||||||
interpreterNotes: normalizeStringArray(source.interpreterNotes).slice(0, 12),
|
|
||||||
loweringNotes: normalizeStringArray(source.loweringNotes).slice(0, 16),
|
|
||||||
cultureNotes: normalizeStringArray(source.cultureNotes).slice(0, 12),
|
|
||||||
personaStateAdjustmentNotes: normalizeStringArray(source.personaStateAdjustmentNotes).slice(0, 12),
|
|
||||||
hypotheses: normalizeStringArray(source.hypotheses).slice(0, 12),
|
|
||||||
doNotDo: normalizeStringArray(source.doNotDo).slice(0, 12),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalizeStringArray(value) {
|
|
||||||
return Array.isArray(value)
|
|
||||||
? value.map((entry) => String(entry).trim()).filter(Boolean)
|
|
||||||
: [];
|
|
||||||
}
|
|
||||||
|
|
||||||
async function runCommand(command, args, stdoutPath, stderrPath) {
|
|
||||||
await appendLog(`exec: ${command} ${args.join(" ")}`);
|
|
||||||
await new Promise((resolvePromise, rejectPromise) => {
|
|
||||||
const child = spawn(command, args, {
|
|
||||||
cwd: repoRoot,
|
|
||||||
stdio: ["ignore", "pipe", "pipe"],
|
|
||||||
windowsHide: true,
|
|
||||||
});
|
|
||||||
const stdout = [];
|
|
||||||
const stderr = [];
|
|
||||||
child.stdout.on("data", (chunk) => stdout.push(chunk));
|
|
||||||
child.stderr.on("data", (chunk) => stderr.push(chunk));
|
|
||||||
child.on("error", rejectPromise);
|
|
||||||
child.on("close", async (code) => {
|
|
||||||
await writeFile(stdoutPath, Buffer.concat(stdout), "utf8");
|
|
||||||
await writeFile(stderrPath, Buffer.concat(stderr), "utf8");
|
|
||||||
if (code !== 0) {
|
|
||||||
rejectPromise(new Error(`${command} exited ${code}; see ${stderrPath}`));
|
|
||||||
} else {
|
|
||||||
resolvePromise();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function writeStatus() {
|
|
||||||
await writeJson(statusPath, runState);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function writeJson(path, value) {
|
|
||||||
await mkdir(dirname(path), { recursive: true });
|
|
||||||
await writeFile(path, `${JSON.stringify(value, null, 2)}\n`, "utf8");
|
|
||||||
}
|
|
||||||
|
|
||||||
async function appendLog(line) {
|
|
||||||
const previous = existsSync(logPath) ? readFileSync(logPath, "utf8") : "";
|
|
||||||
await writeFile(logPath, `${previous}[${new Date().toISOString()}] ${line}\n`, "utf8");
|
|
||||||
}
|
|
||||||
|
|
||||||
function round(value) {
|
|
||||||
return Number(value.toFixed(3));
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseArgs(args) {
|
|
||||||
const parsed = {};
|
|
||||||
for (let index = 0; index < args.length; index += 1) {
|
|
||||||
const arg = args[index];
|
|
||||||
switch (arg) {
|
|
||||||
case "--minutes":
|
|
||||||
parsed.minutes = args[index + 1];
|
|
||||||
index += 1;
|
|
||||||
break;
|
|
||||||
case "--passes":
|
|
||||||
parsed.passes = args[index + 1];
|
|
||||||
index += 1;
|
|
||||||
break;
|
|
||||||
case "--run-id":
|
|
||||||
parsed.runId = args[index + 1];
|
|
||||||
index += 1;
|
|
||||||
break;
|
|
||||||
case "--out-root":
|
|
||||||
parsed.outRoot = args[index + 1];
|
|
||||||
index += 1;
|
|
||||||
break;
|
|
||||||
case "--swarm-root":
|
|
||||||
parsed.swarmRoot = args[index + 1];
|
|
||||||
index += 1;
|
|
||||||
break;
|
|
||||||
case "--initial-prompt-state":
|
|
||||||
parsed.initialPromptState = args[index + 1];
|
|
||||||
index += 1;
|
|
||||||
break;
|
|
||||||
case "--agents":
|
|
||||||
parsed.agents = args[index + 1];
|
|
||||||
index += 1;
|
|
||||||
break;
|
|
||||||
case "--runs-per-worker":
|
|
||||||
parsed.runsPerWorker = args[index + 1];
|
|
||||||
index += 1;
|
|
||||||
break;
|
|
||||||
case "--max-candidates":
|
|
||||||
parsed.maxCandidates = args[index + 1];
|
|
||||||
index += 1;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
throw new Error(`Unknown argument: ${arg}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return parsed;
|
|
||||||
}
|
|
||||||
|
|
@ -1,496 +0,0 @@
|
||||||
#!/usr/bin/env node
|
|
||||||
import { spawn } from "node:child_process";
|
|
||||||
import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
||||||
import { dirname, resolve } from "node:path";
|
|
||||||
|
|
||||||
const repoRoot = resolve(new URL("..", import.meta.url).pathname.replace(/^\/([A-Za-z]:)/, "$1"));
|
|
||||||
const fixtureRoot = resolve(repoRoot, "examples/persona-lowering/metame-casual-pre2025");
|
|
||||||
const voidbotRoot = resolve("E:/Projects/VoidBot");
|
|
||||||
const culturalStackPath = resolve(fixtureRoot, "metame-cultural-stack.yaml");
|
|
||||||
const cultureProfilePaths = [
|
|
||||||
"data/cultural-ontology/cultures/dutch-american-diaspora.yaml",
|
|
||||||
"data/cultural-ontology/cultures/western-europe-maritime-childhood.yaml",
|
|
||||||
"data/cultural-ontology/cultures/portugal-resident-algarve-lisbon.yaml",
|
|
||||||
"data/cultural-ontology/cultures/texas-houston-us-adolescence.yaml",
|
|
||||||
"data/cultural-ontology/subcultures/online-game-dev-discord-pre2025.yaml",
|
|
||||||
"data/cultural-ontology/subcultures/gamecult-open-source-crypto-anarchist.yaml",
|
|
||||||
];
|
|
||||||
const targetOverlayPath = resolve(repoRoot, "data/target-language-ontology/en-US-metacrat-casual-pre2025.yaml");
|
|
||||||
|
|
||||||
const args = new Set(process.argv.slice(2));
|
|
||||||
const real = args.has("--real");
|
|
||||||
const keepTemp = args.has("--keep-temp");
|
|
||||||
const itemFilter = readArg("--item");
|
|
||||||
const limit = Number(readArg("--limit") ?? "0");
|
|
||||||
const runId = readArg("--run-id") ?? new Date().toISOString().replace(/[:.]/g, "-");
|
|
||||||
const outputDir = resolve(fixtureRoot, "runs", runId);
|
|
||||||
|
|
||||||
const fixture = await loadFixture();
|
|
||||||
const items = fixture.items
|
|
||||||
.filter((item) => !itemFilter || item.id === itemFilter)
|
|
||||||
.slice(0, limit > 0 ? limit : undefined);
|
|
||||||
const holdoutUtterances = fixture.items
|
|
||||||
.map((item) => item.groundTruth)
|
|
||||||
.filter((utterance) => utterance.trim().length > 0);
|
|
||||||
|
|
||||||
if (items.length === 0) {
|
|
||||||
throw new Error(`No fixture items selected${itemFilter ? ` for --item ${itemFilter}` : ""}.`);
|
|
||||||
}
|
|
||||||
|
|
||||||
await mkdir(outputDir, { recursive: true });
|
|
||||||
|
|
||||||
const culturalStack = await readFile(culturalStackPath, "utf8");
|
|
||||||
const cultureProfiles = await Promise.all(cultureProfilePaths.map(async (path) => ({
|
|
||||||
path,
|
|
||||||
text: await readFile(resolve(repoRoot, path), "utf8"),
|
|
||||||
})));
|
|
||||||
const targetOverlay = await readFile(targetOverlayPath, "utf8");
|
|
||||||
const personaPrompt = await readFile(resolve(fixtureRoot, "persona-interlingua-prompt.md"), "utf8");
|
|
||||||
const loweringPrompt = await readFile(resolve(fixtureRoot, "weksa-lowering-prompt.md"), "utf8");
|
|
||||||
const baseProjection = await assembleVoidBotPrompt({ itemId: "base" });
|
|
||||||
const projectedMemory = extractBetween(
|
|
||||||
baseProjection.prompt,
|
|
||||||
"What you remember, feel, and want right now:",
|
|
||||||
"Known human pronoun guidance:",
|
|
||||||
).trim();
|
|
||||||
|
|
||||||
const runSummary = {
|
|
||||||
schema_version: "weksa.persona_golf_run.v0",
|
|
||||||
run_id: runId,
|
|
||||||
mode: real ? "real" : "dry_run",
|
|
||||||
fixture_id: "metame-casual-pre2025",
|
|
||||||
target_overlay: "weksa.target_cultural_ontology.en-US.metacrat_casual_pre2025.v0",
|
|
||||||
cultural_stack: "examples/persona-lowering/metame-casual-pre2025/metame-cultural-stack.yaml",
|
|
||||||
items: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
for (const item of items) {
|
|
||||||
const itemDir = resolve(outputDir, item.id);
|
|
||||||
await mkdir(itemDir, { recursive: true });
|
|
||||||
const conversationSurface = renderConversationSurface(item);
|
|
||||||
const memorySurface = renderTemporaryMemorySurface(projectedMemory, item);
|
|
||||||
await writeFile(resolve(itemDir, "conversation-surface.md"), conversationSurface, "utf8");
|
|
||||||
await writeFile(resolve(itemDir, "memory-surface.md"), memorySurface, "utf8");
|
|
||||||
|
|
||||||
const assembled = await assembleVoidBotPrompt({
|
|
||||||
itemId: item.id,
|
|
||||||
memorySurfacePath: resolve(itemDir, "memory-surface.md"),
|
|
||||||
conversationSurfacePath: resolve(itemDir, "conversation-surface.md"),
|
|
||||||
});
|
|
||||||
await writeFile(resolve(itemDir, "voidbot-assembled-prompt.md"), assembled.prompt, "utf8");
|
|
||||||
|
|
||||||
const interlinguaRequest = [
|
|
||||||
personaPrompt.trim(),
|
|
||||||
"",
|
|
||||||
"VoidBot assembled Persona prompt:",
|
|
||||||
"```markdown",
|
|
||||||
assembled.prompt,
|
|
||||||
"```",
|
|
||||||
"",
|
|
||||||
"Fixture item without answer key:",
|
|
||||||
"```yaml",
|
|
||||||
renderItemForPrompt(item),
|
|
||||||
"```",
|
|
||||||
].join("\n");
|
|
||||||
await writeFile(resolve(itemDir, "persona-interlingua-request.md"), interlinguaRequest, "utf8");
|
|
||||||
|
|
||||||
const interlingua = real
|
|
||||||
? await runCodex(interlinguaRequest, { job: `persona-interlingua:${item.id}` })
|
|
||||||
: renderDryRunInterlingua(item);
|
|
||||||
await writeFile(resolve(itemDir, "persona-interlingua.yaml"), interlingua.trim() + "\n", "utf8");
|
|
||||||
|
|
||||||
const loweringRequest = [
|
|
||||||
loweringPrompt.trim(),
|
|
||||||
"",
|
|
||||||
"Projected Metame memory/context surface:",
|
|
||||||
"```markdown",
|
|
||||||
memorySurface,
|
|
||||||
"```",
|
|
||||||
"",
|
|
||||||
"Metame Persona cultural stack reference:",
|
|
||||||
"```yaml",
|
|
||||||
culturalStack,
|
|
||||||
"```",
|
|
||||||
"",
|
|
||||||
"Referenced Weksa culture/subculture profiles:",
|
|
||||||
...cultureProfiles.flatMap((profile) => [
|
|
||||||
"",
|
|
||||||
`# ${profile.path}`,
|
|
||||||
"```yaml",
|
|
||||||
redactHoldouts(profile.text, holdoutUtterances),
|
|
||||||
"```",
|
|
||||||
]),
|
|
||||||
"",
|
|
||||||
"Target-language surface overlay:",
|
|
||||||
"```yaml",
|
|
||||||
redactHoldouts(targetOverlay, holdoutUtterances),
|
|
||||||
"```",
|
|
||||||
"",
|
|
||||||
"Persona-produced interlingua:",
|
|
||||||
"```yaml",
|
|
||||||
interlingua,
|
|
||||||
"```",
|
|
||||||
].join("\n");
|
|
||||||
await writeFile(resolve(itemDir, "weksa-lowering-request.md"), loweringRequest, "utf8");
|
|
||||||
|
|
||||||
const lowered = real
|
|
||||||
? await runCodex(loweringRequest, { job: `weksa-lowering:${item.id}` })
|
|
||||||
: renderDryRunLowering(item);
|
|
||||||
await writeFile(resolve(itemDir, "weksa-lowered-output.yaml"), lowered.trim() + "\n", "utf8");
|
|
||||||
|
|
||||||
const spokenText = extractSpokenText(lowered);
|
|
||||||
const score = scoreText(spokenText, item.groundTruth);
|
|
||||||
const evaluation = renderEvaluation(item, spokenText, score);
|
|
||||||
await writeFile(resolve(itemDir, "evaluation.yaml"), evaluation, "utf8");
|
|
||||||
runSummary.items.push({
|
|
||||||
id: item.id,
|
|
||||||
spoken_text: spokenText,
|
|
||||||
ground_truth: item.groundTruth,
|
|
||||||
token_f1: Number(score.f1.toFixed(3)),
|
|
||||||
exact_normalized: score.exact,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
await writeFile(resolve(outputDir, "summary.json"), `${JSON.stringify(runSummary, null, 2)}\n`, "utf8");
|
|
||||||
process.stdout.write(`${JSON.stringify({ ok: true, outputDir, mode: runSummary.mode, items: runSummary.items }, null, 2)}\n`);
|
|
||||||
|
|
||||||
if (!keepTemp) {
|
|
||||||
await rm(resolve(outputDir, "_tmp"), { recursive: true, force: true }).catch(() => undefined);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadFixture() {
|
|
||||||
const interlinguaText = await readFile(resolve(fixtureRoot, "interlingua.yaml"), "utf8");
|
|
||||||
const groundTruthText = await readFile(resolve(fixtureRoot, "ground-truth.yaml"), "utf8");
|
|
||||||
const itemIds = [...interlinguaText.matchAll(/^ - id: ([^\r\n]+)/gm)].map((match) => match[1].trim());
|
|
||||||
return {
|
|
||||||
items: itemIds.map((id) => {
|
|
||||||
const interlinguaBlock = extractListItemBlock(interlinguaText, id);
|
|
||||||
const groundBlock = extractListItemBlock(groundTruthText, id);
|
|
||||||
return {
|
|
||||||
id,
|
|
||||||
interlinguaBlock,
|
|
||||||
groundTruth: readYamlStringField(groundBlock, "utterance"),
|
|
||||||
messageId: readYamlStringField(groundBlock, "message_id"),
|
|
||||||
channelName: readYamlStringField(groundBlock, "channel_name"),
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function extractListItemBlock(text, id) {
|
|
||||||
const pattern = new RegExp(`^ - id: ${escapeRegExp(id)}\\r?\\n`, "m");
|
|
||||||
const startMatch = pattern.exec(text);
|
|
||||||
if (!startMatch) {
|
|
||||||
throw new Error(`Could not find item ${id}.`);
|
|
||||||
}
|
|
||||||
const start = startMatch.index;
|
|
||||||
const rest = text.slice(start + startMatch[0].length);
|
|
||||||
const next = rest.search(/^ - id: /m);
|
|
||||||
return text.slice(start, next >= 0 ? start + startMatch[0].length + next : text.length).trimEnd();
|
|
||||||
}
|
|
||||||
|
|
||||||
function readYamlStringField(block, key) {
|
|
||||||
const quoted = new RegExp(`^ ${key}: "([\\s\\S]*?)"\\r?$`, "m").exec(block);
|
|
||||||
if (quoted) {
|
|
||||||
return quoted[1].replace(/\\"/g, "\"");
|
|
||||||
}
|
|
||||||
const plain = new RegExp(`^ ${key}: ([^\\r\\n]+)`, "m").exec(block);
|
|
||||||
return plain ? plain[1].trim() : "";
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderConversationSurface(item) {
|
|
||||||
return [
|
|
||||||
"Read this as the current fixture room, not as old archive trivia.",
|
|
||||||
"The Persona should decide intent from this local scene. Do not reveal or infer the held-out answer key.",
|
|
||||||
"",
|
|
||||||
`Current fixture item: ${item.id}`,
|
|
||||||
"```yaml",
|
|
||||||
item.interlinguaBlock,
|
|
||||||
"```",
|
|
||||||
].join("\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderTemporaryMemorySurface(projectedMemory, item) {
|
|
||||||
return [
|
|
||||||
projectedMemory,
|
|
||||||
"",
|
|
||||||
"Temporary fixture adjustment:",
|
|
||||||
`- Treat the ${item.id} scene as the live room situation for this private harness turn.`,
|
|
||||||
"- The goal is to produce interlingua intent for Weksa, not a final Discord line.",
|
|
||||||
"- Keep pre-2025 casual room texture available: compact banter, quick self-deprecation, friendly status play, and casual turn-taking.",
|
|
||||||
].join("\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderItemForPrompt(item) {
|
|
||||||
return item.interlinguaBlock;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function assembleVoidBotPrompt({ itemId, memorySurfacePath, conversationSurfacePath }) {
|
|
||||||
const tempDir = resolve(outputDir, "_tmp", itemId);
|
|
||||||
await mkdir(tempDir, { recursive: true });
|
|
||||||
const outPath = resolve(tempDir, "assembled.md");
|
|
||||||
const args = [
|
|
||||||
"node_modules/tsx/dist/cli.mjs",
|
|
||||||
"scripts/run-repo-face-heartbeats.ts",
|
|
||||||
"--assemble-prompt",
|
|
||||||
"metame",
|
|
||||||
"--out",
|
|
||||||
outPath,
|
|
||||||
];
|
|
||||||
if (memorySurfacePath) {
|
|
||||||
args.push("--memory-surface", memorySurfacePath);
|
|
||||||
}
|
|
||||||
if (conversationSurfacePath) {
|
|
||||||
args.push("--conversation-surface", conversationSurfacePath);
|
|
||||||
}
|
|
||||||
const run = await runCommand(process.execPath, args, { cwd: voidbotRoot, timeoutMs: 240_000 });
|
|
||||||
if (run.code !== 0) {
|
|
||||||
throw new Error(`VoidBot prompt assembly failed for ${itemId}:\n${run.stderr || run.stdout}`);
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
result: JSON.parse(run.stdout.trim()),
|
|
||||||
prompt: await readFile(outPath, "utf8"),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async function runCodex(prompt, { job }) {
|
|
||||||
const args = [
|
|
||||||
"exec",
|
|
||||||
"-m",
|
|
||||||
process.env.WEKSA_GOLF_MODEL ?? "gpt-5.4",
|
|
||||||
"-c",
|
|
||||||
'approval_policy="never"',
|
|
||||||
"-c",
|
|
||||||
'model_reasoning_effort="low"',
|
|
||||||
"--json",
|
|
||||||
"--skip-git-repo-check",
|
|
||||||
"-s",
|
|
||||||
"read-only",
|
|
||||||
"-",
|
|
||||||
];
|
|
||||||
const command = process.env.WEKSA_CODEX_EXECUTABLE ?? "C:\\Users\\Meta\\AppData\\Roaming\\npm\\codex.cmd";
|
|
||||||
const run = await runCommand(command, args, {
|
|
||||||
cwd: repoRoot,
|
|
||||||
input: prompt,
|
|
||||||
timeoutMs: Number(process.env.WEKSA_GOLF_TIMEOUT_MS ?? 240_000),
|
|
||||||
});
|
|
||||||
await writeFile(resolve(outputDir, `${sanitizeJob(job)}.stdout.jsonl`), run.stdout, "utf8");
|
|
||||||
await writeFile(resolve(outputDir, `${sanitizeJob(job)}.stderr.txt`), run.stderr, "utf8");
|
|
||||||
if (run.code !== 0) {
|
|
||||||
throw new Error(`Codex generation failed for ${job}:\n${run.stderr || run.stdout}`);
|
|
||||||
}
|
|
||||||
const text = extractLastCodexAgentMessage(run.stdout).trim();
|
|
||||||
if (!text) {
|
|
||||||
throw new Error(`Codex generation produced no final text for ${job}.`);
|
|
||||||
}
|
|
||||||
return text;
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderDryRunInterlingua(item) {
|
|
||||||
return [
|
|
||||||
"interlingua_version: weksa.interlingua.v0",
|
|
||||||
`packet_id: metame-casual-pre2025-${item.id}`,
|
|
||||||
"kind: persona_discord_turn_intent",
|
|
||||||
"provenance:",
|
|
||||||
" produced_by: dry_run_fixture_from_existing_interlingua",
|
|
||||||
"context:",
|
|
||||||
" medium: Discord",
|
|
||||||
" fixture_id: metame-casual-pre2025",
|
|
||||||
` item_id: ${item.id}`,
|
|
||||||
"discourse:",
|
|
||||||
...indentBlock(item.interlinguaBlock, " "),
|
|
||||||
"constraints:",
|
|
||||||
" lowering:",
|
|
||||||
" target_profile: weksa.target_cultural_ontology.en-US.metacrat_casual_pre2025.v0",
|
|
||||||
" avoid_surface_copy: true",
|
|
||||||
].join("\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderDryRunLowering(item) {
|
|
||||||
return [
|
|
||||||
"schema_version: weksa.persona_culture_lowering_output.v0",
|
|
||||||
"fixture_id: metame-casual-pre2025",
|
|
||||||
`item_id: ${item.id}`,
|
|
||||||
`spoken_text: ${JSON.stringify(item.groundTruth)}`,
|
|
||||||
"trace:",
|
|
||||||
" activated_profiles:",
|
|
||||||
" - weksa.target_cultural_ontology.en-US.metacrat_casual_pre2025.v0",
|
|
||||||
" activated_affordances:",
|
|
||||||
" - dry_run_answer_key_oracle",
|
|
||||||
" notes:",
|
|
||||||
" - Dry run uses answer key only to verify harness plumbing. Use --real for model golf.",
|
|
||||||
].join("\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
function redactHoldouts(text, holdouts) {
|
|
||||||
let redacted = text;
|
|
||||||
for (const holdout of holdouts) {
|
|
||||||
redacted = replaceAllLiteral(redacted, holdout, "[held-out fixture utterance redacted]");
|
|
||||||
redacted = replaceAllLiteral(redacted, holdout.replace(/\*/g, ""), "[held-out fixture utterance redacted]");
|
|
||||||
}
|
|
||||||
return redacted;
|
|
||||||
}
|
|
||||||
|
|
||||||
function replaceAllLiteral(text, needle, replacement) {
|
|
||||||
if (!needle) {
|
|
||||||
return text;
|
|
||||||
}
|
|
||||||
return text.split(needle).join(replacement);
|
|
||||||
}
|
|
||||||
|
|
||||||
function extractSpokenText(text) {
|
|
||||||
const match = /^spoken_text:\s*(.+)$/m.exec(text);
|
|
||||||
if (!match) {
|
|
||||||
return text.trim().split(/\r?\n/).find((line) => line.trim().length > 0)?.trim() ?? "";
|
|
||||||
}
|
|
||||||
const value = match[1].trim();
|
|
||||||
if (value.startsWith("\"")) {
|
|
||||||
try {
|
|
||||||
return JSON.parse(value);
|
|
||||||
} catch {
|
|
||||||
return value.replace(/^"|"$/g, "");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderEvaluation(item, spokenText, score) {
|
|
||||||
return [
|
|
||||||
"schema_version: weksa.persona_golf_item_evaluation.v0",
|
|
||||||
`fixture_id: metame-casual-pre2025`,
|
|
||||||
`item_id: ${item.id}`,
|
|
||||||
`message_id: "${item.messageId}"`,
|
|
||||||
`generated: ${JSON.stringify(spokenText)}`,
|
|
||||||
`ground_truth: ${JSON.stringify(item.groundTruth)}`,
|
|
||||||
"score:",
|
|
||||||
` exact_normalized: ${score.exact}`,
|
|
||||||
` token_precision: ${score.precision.toFixed(3)}`,
|
|
||||||
` token_recall: ${score.recall.toFixed(3)}`,
|
|
||||||
` token_f1: ${score.f1.toFixed(3)}`,
|
|
||||||
].join("\n") + "\n";
|
|
||||||
}
|
|
||||||
|
|
||||||
function scoreText(generated, truth) {
|
|
||||||
const left = normalizeForScore(generated);
|
|
||||||
const right = normalizeForScore(truth);
|
|
||||||
const leftTokens = left.split(/\s+/).filter(Boolean);
|
|
||||||
const rightTokens = right.split(/\s+/).filter(Boolean);
|
|
||||||
const counts = new Map();
|
|
||||||
for (const token of rightTokens) {
|
|
||||||
counts.set(token, (counts.get(token) ?? 0) + 1);
|
|
||||||
}
|
|
||||||
let overlap = 0;
|
|
||||||
for (const token of leftTokens) {
|
|
||||||
const count = counts.get(token) ?? 0;
|
|
||||||
if (count > 0) {
|
|
||||||
overlap += 1;
|
|
||||||
counts.set(token, count - 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const precision = leftTokens.length ? overlap / leftTokens.length : 0;
|
|
||||||
const recall = rightTokens.length ? overlap / rightTokens.length : 0;
|
|
||||||
const f1 = precision + recall ? (2 * precision * recall) / (precision + recall) : 0;
|
|
||||||
return { exact: left === right, precision, recall, f1 };
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalizeForScore(value) {
|
|
||||||
return value
|
|
||||||
.toLowerCase()
|
|
||||||
.replace(/[*_`~.,!?;:'"()[\]{}]/g, "")
|
|
||||||
.replace(/\s+/g, " ")
|
|
||||||
.trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
function extractBetween(text, startMarker, endMarker) {
|
|
||||||
const start = text.indexOf(startMarker);
|
|
||||||
const end = text.indexOf(endMarker, start + startMarker.length);
|
|
||||||
if (start < 0 || end < 0 || end <= start) {
|
|
||||||
throw new Error(`Could not extract section ${startMarker}`);
|
|
||||||
}
|
|
||||||
return text.slice(start + startMarker.length, end);
|
|
||||||
}
|
|
||||||
|
|
||||||
function runCommand(command, args, { cwd, input = "", timeoutMs = 120_000 }) {
|
|
||||||
return new Promise((resolveRun) => {
|
|
||||||
const isCmdShim = /\.cmd$/i.test(command) || /\.bat$/i.test(command);
|
|
||||||
const spawnCommand = isCmdShim ? (process.env.ComSpec ?? "cmd.exe") : command;
|
|
||||||
const spawnArgs = isCmdShim ? ["/d", "/s", "/c", command, ...args] : args;
|
|
||||||
const child = spawn(spawnCommand, spawnArgs, {
|
|
||||||
cwd,
|
|
||||||
env: process.env,
|
|
||||||
stdio: ["pipe", "pipe", "pipe"],
|
|
||||||
windowsHide: true,
|
|
||||||
});
|
|
||||||
let stdout = "";
|
|
||||||
let stderr = "";
|
|
||||||
const timer = setTimeout(() => child.kill(), timeoutMs);
|
|
||||||
child.stdout.setEncoding("utf8");
|
|
||||||
child.stderr.setEncoding("utf8");
|
|
||||||
child.stdout.on("data", (chunk) => { stdout += chunk; });
|
|
||||||
child.stderr.on("data", (chunk) => { stderr += chunk; });
|
|
||||||
child.on("error", (error) => {
|
|
||||||
clearTimeout(timer);
|
|
||||||
resolveRun({ code: -1, stdout, stderr: `${stderr}\n${error.message}` });
|
|
||||||
});
|
|
||||||
child.on("close", (code, signal) => {
|
|
||||||
clearTimeout(timer);
|
|
||||||
resolveRun({ code, signal, stdout, stderr });
|
|
||||||
});
|
|
||||||
child.stdin.end(input);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function extractLastCodexAgentMessage(stdout) {
|
|
||||||
let last = "";
|
|
||||||
for (const line of stdout.split(/\r?\n/)) {
|
|
||||||
if (!line.trim()) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const event = JSON.parse(line);
|
|
||||||
const message = event?.msg?.message ?? event?.message ?? event?.text;
|
|
||||||
if (typeof message === "string" && message.trim()) {
|
|
||||||
last = message;
|
|
||||||
}
|
|
||||||
const itemText = event?.item?.text;
|
|
||||||
if (typeof itemText === "string" && itemText.trim()) {
|
|
||||||
last = itemText;
|
|
||||||
}
|
|
||||||
const content = event?.item?.content;
|
|
||||||
if (Array.isArray(content)) {
|
|
||||||
const text = content
|
|
||||||
.map((part) => typeof part?.text === "string" ? part.text : "")
|
|
||||||
.join("\n")
|
|
||||||
.trim();
|
|
||||||
if (text) {
|
|
||||||
last = text;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
if (line.trim()) {
|
|
||||||
last = line.trim();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return last;
|
|
||||||
}
|
|
||||||
|
|
||||||
function indentBlock(block, prefix) {
|
|
||||||
return block.split(/\r?\n/).map((line) => `${prefix}${line}`).join("\n").split(/\r?\n/);
|
|
||||||
}
|
|
||||||
|
|
||||||
function readArg(name) {
|
|
||||||
const index = process.argv.indexOf(name);
|
|
||||||
if (index < 0) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
return process.argv[index + 1];
|
|
||||||
}
|
|
||||||
|
|
||||||
function escapeRegExp(value) {
|
|
||||||
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
||||||
}
|
|
||||||
|
|
||||||
function sanitizeJob(value) {
|
|
||||||
return value.replace(/[^A-Za-z0-9_.-]+/g, "_");
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,83 +0,0 @@
|
||||||
param(
|
|
||||||
[int] $Port = 8813,
|
|
||||||
[string] $HostName = "127.0.0.1",
|
|
||||||
[string] $StateRoot = "E:\Projects\weksa\.weksa",
|
|
||||||
[string] $MiMoApiKeyPath = "E:\Projects\gamecult-ops\mimo-api-Weksa.txt",
|
|
||||||
[string] $IdunnRudpHealth = "127.0.0.1:17870",
|
|
||||||
[string] $IdunnDaemon = "weksa",
|
|
||||||
[string] $IdunnHealthContract = "weksa.cultnet-rudp-provider-health",
|
|
||||||
[string] $OdinCultMeshRudp = "127.0.0.1:17871"
|
|
||||||
)
|
|
||||||
|
|
||||||
$ErrorActionPreference = "Stop"
|
|
||||||
|
|
||||||
$repoRoot = Split-Path -Parent $PSScriptRoot
|
|
||||||
$pidPath = Join-Path $StateRoot "weksa-daemon.pid"
|
|
||||||
$outLog = Join-Path $StateRoot "weksa-daemon.out.log"
|
|
||||||
$errLog = Join-Path $StateRoot "weksa-daemon.err.log"
|
|
||||||
|
|
||||||
New-Item -ItemType Directory -Force -Path $StateRoot | Out-Null
|
|
||||||
$cultLibPackages = Resolve-Path -LiteralPath (Join-Path $repoRoot "..\CultLib\packages")
|
|
||||||
$env:NODE_PATH = (@($cultLibPackages.Path, $env:NODE_PATH) | Where-Object { $_ }) -join [IO.Path]::PathSeparator
|
|
||||||
|
|
||||||
if (Test-Path -LiteralPath $pidPath) {
|
|
||||||
$pidText = (Get-Content -LiteralPath $pidPath -Raw).Trim()
|
|
||||||
if ($pidText -match "^\d+$") {
|
|
||||||
$existing = Get-Process -Id ([int] $pidText) -ErrorAction SilentlyContinue
|
|
||||||
if ($null -ne $existing) {
|
|
||||||
try {
|
|
||||||
& node (Join-Path $repoRoot "scripts\weksa-daemon.mjs") --health --host $HostName --port $Port --state-root $StateRoot | Out-Null
|
|
||||||
if ($LASTEXITCODE -eq 0) {
|
|
||||||
Write-Host "Weksa daemon already healthy as PID $pidText."
|
|
||||||
return
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
Stop-Process -Id $existing.Id -Force -ErrorAction SilentlyContinue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Remove-Item -LiteralPath $pidPath -Force -ErrorAction SilentlyContinue
|
|
||||||
}
|
|
||||||
|
|
||||||
$arguments = @(
|
|
||||||
(Join-Path $repoRoot "scripts\weksa-daemon.mjs"),
|
|
||||||
"--host", $HostName,
|
|
||||||
"--port", [string] $Port,
|
|
||||||
"--state-root", $StateRoot,
|
|
||||||
"--mimo-api-key-path", $MiMoApiKeyPath,
|
|
||||||
"--idunn-rudp-health", $IdunnRudpHealth,
|
|
||||||
"--idunn-daemon", $IdunnDaemon,
|
|
||||||
"--idunn-health-contract", $IdunnHealthContract
|
|
||||||
)
|
|
||||||
|
|
||||||
if (-not [string]::IsNullOrWhiteSpace($OdinCultMeshRudp)) {
|
|
||||||
$arguments += @("--odin-cultmesh-rudp", $OdinCultMeshRudp)
|
|
||||||
}
|
|
||||||
|
|
||||||
$process = Start-Process -FilePath "node" `
|
|
||||||
-ArgumentList $arguments `
|
|
||||||
-WorkingDirectory $repoRoot `
|
|
||||||
-WindowStyle Hidden `
|
|
||||||
-PassThru `
|
|
||||||
-RedirectStandardOutput $outLog `
|
|
||||||
-RedirectStandardError $errLog
|
|
||||||
|
|
||||||
$process.Id | Set-Content -Encoding ASCII -LiteralPath $pidPath
|
|
||||||
|
|
||||||
Start-Sleep -Seconds 2
|
|
||||||
if ($process.HasExited) {
|
|
||||||
$detail = ""
|
|
||||||
if (Test-Path -LiteralPath $outLog) { $detail += Get-Content -Raw -LiteralPath $outLog }
|
|
||||||
if (Test-Path -LiteralPath $errLog) { $detail += Get-Content -Raw -LiteralPath $errLog }
|
|
||||||
throw "Weksa daemon exited immediately with code $($process.ExitCode).`n$detail"
|
|
||||||
}
|
|
||||||
|
|
||||||
& node (Join-Path $repoRoot "scripts\weksa-daemon.mjs") --health --host $HostName --port $Port --state-root $StateRoot | Out-Null
|
|
||||||
if ($LASTEXITCODE -ne 0) {
|
|
||||||
throw "Weksa daemon started but health check failed."
|
|
||||||
}
|
|
||||||
|
|
||||||
Write-Host "Started Weksa daemon as PID $($process.Id)."
|
|
||||||
Write-Host " command: rudp://$HostName`:$Port"
|
|
||||||
Write-Host " Odin CultMesh/RUDP: $OdinCultMeshRudp"
|
|
||||||
Write-Host " state: $StateRoot"
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,466 +0,0 @@
|
||||||
"use strict";
|
|
||||||
|
|
||||||
const dgram = require("dgram");
|
|
||||||
const path = require("path");
|
|
||||||
|
|
||||||
const {
|
|
||||||
CultNetRudpSession,
|
|
||||||
decodeRudpPacket,
|
|
||||||
encodeCultNetMessageForWire,
|
|
||||||
encodeRudpPacket,
|
|
||||||
parseCultNetMessage,
|
|
||||||
} = require("cultnet-ts");
|
|
||||||
|
|
||||||
const { encode, decode } = require(path.resolve(
|
|
||||||
__dirname,
|
|
||||||
"..",
|
|
||||||
"..",
|
|
||||||
"CultLib",
|
|
||||||
"node_modules",
|
|
||||||
"@msgpack",
|
|
||||||
"msgpack",
|
|
||||||
));
|
|
||||||
|
|
||||||
const CULTNET_RUDP_PROTOCOL_ID = "cultnet.transport.rudp.v0";
|
|
||||||
const WEKSA_COMMAND_RUDP_CONNECTION_ID = 0x1d0d0002;
|
|
||||||
const WEKSA_MIMO_VOICEDESIGN_COMMAND_SCHEMA_ID = "weksa.mimo_voicedesign_command.v0";
|
|
||||||
const WEKSA_MIMO_VOICEDESIGN_RECEIPT_SCHEMA_ID = "weksa.mimo_voicedesign_receipt.v0";
|
|
||||||
|
|
||||||
async function createWeksaRudpCommandServer(options) {
|
|
||||||
if (!options?.host || !options?.port || typeof options.onCommand !== "function") {
|
|
||||||
throw new Error("Weksa RUDP command server requires host, port, and onCommand.");
|
|
||||||
}
|
|
||||||
const socket = dgram.createSocket(endpointFamily(options.host));
|
|
||||||
await bindServerSocket(socket, options.host, options.port);
|
|
||||||
|
|
||||||
const sessions = new Map();
|
|
||||||
const resendTimer = setInterval(() => {
|
|
||||||
for (const entry of sessions.values()) {
|
|
||||||
for (const packet of entry.session.dueResends(Date.now())) {
|
|
||||||
void sendPacket(socket, entry.endpoint, packet);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, 25);
|
|
||||||
resendTimer.unref?.();
|
|
||||||
|
|
||||||
const onMessage = (wire, remote) => {
|
|
||||||
void handleServerDatagram({
|
|
||||||
wire,
|
|
||||||
remote,
|
|
||||||
socket,
|
|
||||||
sessions,
|
|
||||||
options,
|
|
||||||
}).catch((error) => {
|
|
||||||
options.onError?.(error);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
const onError = (error) => {
|
|
||||||
options.onError?.(error instanceof Error ? error : new Error(String(error)));
|
|
||||||
};
|
|
||||||
|
|
||||||
socket.on("message", onMessage);
|
|
||||||
socket.on("error", onError);
|
|
||||||
|
|
||||||
return {
|
|
||||||
endpoint: { host: options.host, port: options.port },
|
|
||||||
uri: formatRudpEndpoint(options.host, options.port),
|
|
||||||
close() {
|
|
||||||
clearInterval(resendTimer);
|
|
||||||
socket.off("message", onMessage);
|
|
||||||
socket.off("error", onError);
|
|
||||||
socket.close();
|
|
||||||
sessions.clear();
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleServerDatagram(input) {
|
|
||||||
const { wire, remote, socket, sessions, options } = input;
|
|
||||||
const packet = decodeRudpPacket(wire);
|
|
||||||
const key = endpointKey(remote.address, remote.port);
|
|
||||||
let entry = sessions.get(key);
|
|
||||||
if (!entry) {
|
|
||||||
if (packet.packetType !== "connect") {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const session = new CultNetRudpSession({
|
|
||||||
connectionId: WEKSA_COMMAND_RUDP_CONNECTION_ID,
|
|
||||||
initialSequence: 1,
|
|
||||||
resendDelayMs: 100,
|
|
||||||
maxPendingReliablePackets: 64,
|
|
||||||
});
|
|
||||||
entry = {
|
|
||||||
session,
|
|
||||||
endpoint: { host: remote.address, port: remote.port },
|
|
||||||
};
|
|
||||||
sessions.set(key, entry);
|
|
||||||
await sendPacket(socket, entry.endpoint, session.acceptConnect(packet, Date.now()));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = entry.session.receive(packet, Date.now());
|
|
||||||
if (result.reply) {
|
|
||||||
await sendPacket(socket, entry.endpoint, result.reply);
|
|
||||||
}
|
|
||||||
for (const frame of result.delivered) {
|
|
||||||
await handleServerFrame({
|
|
||||||
socket,
|
|
||||||
entry,
|
|
||||||
frame,
|
|
||||||
options,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (packet.packetType === "accept" || result.delivered.length > 0) {
|
|
||||||
await sendPacket(socket, entry.endpoint, entry.session.createAck());
|
|
||||||
}
|
|
||||||
if (result.disconnected) {
|
|
||||||
sessions.delete(key);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleServerFrame(input) {
|
|
||||||
const { socket, entry, frame, options } = input;
|
|
||||||
if (frame.channelId !== "schema") {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const message = parseCultNetMessage(decode(frame.payload), "cultnet.schema.v0");
|
|
||||||
if (message.schemaVersion !== "cultnet.document_put_raw.v0") {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (message.document.schemaId !== WEKSA_MIMO_VOICEDESIGN_COMMAND_SCHEMA_ID) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const payload = decode(message.document.payload);
|
|
||||||
const command = {
|
|
||||||
...payload,
|
|
||||||
request_id: payload?.request_id ?? message.document.recordKey,
|
|
||||||
};
|
|
||||||
let receipt;
|
|
||||||
try {
|
|
||||||
receipt = await options.onCommand(command, message);
|
|
||||||
} catch (error) {
|
|
||||||
options.onError?.(error instanceof Error ? error : new Error(String(error)));
|
|
||||||
receipt = {
|
|
||||||
schema_version: WEKSA_MIMO_VOICEDESIGN_RECEIPT_SCHEMA_ID,
|
|
||||||
request_id: command.request_id,
|
|
||||||
generated_at: new Date().toISOString(),
|
|
||||||
ok: false,
|
|
||||||
error: error instanceof Error ? error.message : String(error),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
if (!receipt) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const receiptMessage = {
|
|
||||||
schemaVersion: "cultnet.document_put_raw.v0",
|
|
||||||
messageId: `${options.runtimeId ?? "weksa-daemon"}:receipt:${receipt.request_id}`,
|
|
||||||
document: {
|
|
||||||
schemaId: WEKSA_MIMO_VOICEDESIGN_RECEIPT_SCHEMA_ID,
|
|
||||||
recordKey: receipt.request_id,
|
|
||||||
storedAt: receipt.generated_at ?? receipt.observed_at ?? new Date().toISOString(),
|
|
||||||
payloadEncoding: "messagepack",
|
|
||||||
payload: encode(receipt),
|
|
||||||
sourceRuntimeId: options.runtimeId ?? "weksa-daemon",
|
|
||||||
sourceRole: "weksa-command-ingress",
|
|
||||||
tags: [CULTNET_RUDP_PROTOCOL_ID, "speech_provider.mimo.voicedesign"],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
const wirePayload = encode(
|
|
||||||
encodeCultNetMessageForWire(receiptMessage, "cultnet.schema.v0"),
|
|
||||||
);
|
|
||||||
for (const packet of entry.session.sendMany("schema", wirePayload, {
|
|
||||||
reliable: true,
|
|
||||||
ordered: true,
|
|
||||||
nowMs: Date.now(),
|
|
||||||
})) {
|
|
||||||
await sendPacket(socket, entry.endpoint, packet);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function sendWeksaMimoVoiceDesignCommand(options) {
|
|
||||||
const endpoint = parseEndpoint(options?.endpoint);
|
|
||||||
const requestId = options.command?.request_id ?? buildRequestId();
|
|
||||||
const command = {
|
|
||||||
...options.command,
|
|
||||||
request_id: requestId,
|
|
||||||
};
|
|
||||||
const socket = dgram.createSocket(endpointFamily(endpoint.host));
|
|
||||||
await bindClientSocket(socket, options?.localHost);
|
|
||||||
const receiver = createPacketReceiver(socket);
|
|
||||||
const session = new CultNetRudpSession({
|
|
||||||
connectionId: WEKSA_COMMAND_RUDP_CONNECTION_ID,
|
|
||||||
initialSequence: 1,
|
|
||||||
resendDelayMs: 100,
|
|
||||||
maxPendingReliablePackets: 64,
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
|
||||||
await sendPacket(socket, endpoint, session.createConnect(Date.now(), new Uint8Array()));
|
|
||||||
await receiveCultNetMessageUntil({
|
|
||||||
receiver,
|
|
||||||
session,
|
|
||||||
endpoint,
|
|
||||||
timeoutMs: options.timeoutMs ?? 5000,
|
|
||||||
label: "accept",
|
|
||||||
predicate: (_message, packet) => packet.packetType === "accept",
|
|
||||||
});
|
|
||||||
|
|
||||||
const commandMessage = {
|
|
||||||
schemaVersion: "cultnet.document_put_raw.v0",
|
|
||||||
messageId: `${options.runtimeId ?? "weksa-cli"}:command:${requestId}`,
|
|
||||||
document: {
|
|
||||||
schemaId: WEKSA_MIMO_VOICEDESIGN_COMMAND_SCHEMA_ID,
|
|
||||||
recordKey: requestId,
|
|
||||||
storedAt: new Date().toISOString(),
|
|
||||||
payloadEncoding: "messagepack",
|
|
||||||
payload: encode(command),
|
|
||||||
sourceRuntimeId: options.runtimeId ?? "weksa-cli",
|
|
||||||
sourceRole: "command-client",
|
|
||||||
tags: [CULTNET_RUDP_PROTOCOL_ID, "speech_provider.mimo.voicedesign"],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
const wirePayload = encode(
|
|
||||||
encodeCultNetMessageForWire(commandMessage, "cultnet.schema.v0"),
|
|
||||||
);
|
|
||||||
for (const packet of session.sendMany("schema", wirePayload, {
|
|
||||||
reliable: true,
|
|
||||||
ordered: true,
|
|
||||||
nowMs: Date.now(),
|
|
||||||
})) {
|
|
||||||
await sendPacket(socket, endpoint, packet);
|
|
||||||
}
|
|
||||||
|
|
||||||
return await receiveCultNetMessageUntil({
|
|
||||||
receiver,
|
|
||||||
session,
|
|
||||||
endpoint,
|
|
||||||
timeoutMs: options.timeoutMs ?? 10000,
|
|
||||||
label: "receipt",
|
|
||||||
predicate: (message) =>
|
|
||||||
message.schemaVersion === "cultnet.document_put_raw.v0"
|
|
||||||
&& message.document.schemaId === WEKSA_MIMO_VOICEDESIGN_RECEIPT_SCHEMA_ID
|
|
||||||
&& message.document.recordKey === requestId,
|
|
||||||
decodePayload: true,
|
|
||||||
});
|
|
||||||
} finally {
|
|
||||||
receiver.close();
|
|
||||||
socket.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function receiveCultNetMessageUntil(options) {
|
|
||||||
const deadline = Date.now() + options.timeoutMs;
|
|
||||||
while (Date.now() < deadline) {
|
|
||||||
try {
|
|
||||||
const packet = await options.receiver.next(
|
|
||||||
Math.min(100, Math.max(1, deadline - Date.now())),
|
|
||||||
options.label,
|
|
||||||
);
|
|
||||||
const result = options.session.receive(packet, Date.now());
|
|
||||||
if (result.reply) {
|
|
||||||
await sendPacket(options.receiver.socket, options.endpoint, result.reply);
|
|
||||||
}
|
|
||||||
if (packet.packetType === "accept" || result.delivered.length > 0) {
|
|
||||||
await sendPacket(
|
|
||||||
options.receiver.socket,
|
|
||||||
options.endpoint,
|
|
||||||
options.session.createAck(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (packet.packetType === "accept" && options.predicate(undefined, packet)) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
for (const frame of result.delivered) {
|
|
||||||
if (frame.channelId !== "schema") {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const message = parseCultNetMessage(decode(frame.payload), "cultnet.schema.v0");
|
|
||||||
if (!options.predicate(message, packet)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
options.decodePayload
|
|
||||||
&& message.schemaVersion === "cultnet.document_put_raw.v0"
|
|
||||||
) {
|
|
||||||
return decode(message.document.payload);
|
|
||||||
}
|
|
||||||
return message;
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
if (error.code !== "ETIMEDOUT") {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (const packet of options.session.dueResends(Date.now())) {
|
|
||||||
await sendPacket(options.receiver.socket, options.endpoint, packet);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
throw new Error(`timed out waiting for Weksa RUDP ${options.label} after ${options.timeoutMs}ms`);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function bindServerSocket(socket, host, port) {
|
|
||||||
await new Promise((resolve, reject) => {
|
|
||||||
socket.once("error", reject);
|
|
||||||
socket.bind(port, host, () => {
|
|
||||||
socket.off("error", reject);
|
|
||||||
resolve();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function bindClientSocket(socket, localHost) {
|
|
||||||
const bindHost = localHost ?? "0.0.0.0";
|
|
||||||
await new Promise((resolve, reject) => {
|
|
||||||
socket.once("error", reject);
|
|
||||||
socket.bind(0, bindHost, () => {
|
|
||||||
socket.off("error", reject);
|
|
||||||
resolve();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function createPacketReceiver(socket) {
|
|
||||||
const packets = [];
|
|
||||||
const waiters = [];
|
|
||||||
const errors = [];
|
|
||||||
|
|
||||||
const resolveNext = () => {
|
|
||||||
while (waiters.length > 0 && (packets.length > 0 || errors.length > 0)) {
|
|
||||||
const waiter = waiters.shift();
|
|
||||||
clearTimeout(waiter.timer);
|
|
||||||
if (errors.length > 0) {
|
|
||||||
waiter.reject(errors.shift());
|
|
||||||
} else {
|
|
||||||
waiter.resolve(packets.shift());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const onMessage = (wire) => {
|
|
||||||
try {
|
|
||||||
packets.push(decodeRudpPacket(wire));
|
|
||||||
} catch (error) {
|
|
||||||
errors.push(error);
|
|
||||||
}
|
|
||||||
resolveNext();
|
|
||||||
};
|
|
||||||
const onError = (error) => {
|
|
||||||
errors.push(error);
|
|
||||||
resolveNext();
|
|
||||||
};
|
|
||||||
|
|
||||||
socket.on("message", onMessage);
|
|
||||||
socket.on("error", onError);
|
|
||||||
|
|
||||||
return {
|
|
||||||
socket,
|
|
||||||
next(timeoutMs, label = "packet") {
|
|
||||||
if (packets.length > 0) {
|
|
||||||
return Promise.resolve(packets.shift());
|
|
||||||
}
|
|
||||||
if (errors.length > 0) {
|
|
||||||
return Promise.reject(errors.shift());
|
|
||||||
}
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const waiter = {
|
|
||||||
resolve,
|
|
||||||
reject,
|
|
||||||
timer: setTimeout(() => {
|
|
||||||
const index = waiters.indexOf(waiter);
|
|
||||||
if (index >= 0) {
|
|
||||||
waiters.splice(index, 1);
|
|
||||||
}
|
|
||||||
const error = new Error(`timed out waiting for Weksa RUDP ${label}`);
|
|
||||||
error.code = "ETIMEDOUT";
|
|
||||||
reject(error);
|
|
||||||
}, Math.max(1, timeoutMs)),
|
|
||||||
};
|
|
||||||
waiters.push(waiter);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
close() {
|
|
||||||
socket.off("message", onMessage);
|
|
||||||
socket.off("error", onError);
|
|
||||||
while (waiters.length > 0) {
|
|
||||||
const waiter = waiters.shift();
|
|
||||||
clearTimeout(waiter.timer);
|
|
||||||
const error = new Error("Weksa RUDP command receiver closed.");
|
|
||||||
error.code = "ECLOSED";
|
|
||||||
waiter.reject(error);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async function sendPacket(socket, endpoint, packet) {
|
|
||||||
const wire = encodeRudpPacket(packet);
|
|
||||||
await new Promise((resolve, reject) => {
|
|
||||||
socket.send(wire, endpoint.port, endpoint.host, (error) => {
|
|
||||||
if (error) {
|
|
||||||
reject(error);
|
|
||||||
} else {
|
|
||||||
resolve();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseEndpoint(value) {
|
|
||||||
if (!value) {
|
|
||||||
throw new Error("Weksa RUDP endpoint is required.");
|
|
||||||
}
|
|
||||||
const text = String(value).trim();
|
|
||||||
if (text.toLowerCase().startsWith("rudp://")) {
|
|
||||||
const parsed = new URL(text);
|
|
||||||
return {
|
|
||||||
host: parsed.hostname,
|
|
||||||
port: parsePort(parsed.port),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
const ipv6 = text.match(/^\[([^\]]+)\]:(\d+)$/);
|
|
||||||
if (ipv6) {
|
|
||||||
return { host: ipv6[1], port: parsePort(ipv6[2]) };
|
|
||||||
}
|
|
||||||
const index = text.lastIndexOf(":");
|
|
||||||
if (index <= 0) {
|
|
||||||
throw new Error(`Weksa RUDP endpoint must be host:port, got "${value}".`);
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
host: text.slice(0, index),
|
|
||||||
port: parsePort(text.slice(index + 1)),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function parsePort(value) {
|
|
||||||
const port = Number(value);
|
|
||||||
if (!Number.isInteger(port) || port <= 0 || port > 65535) {
|
|
||||||
throw new Error(`Weksa RUDP endpoint port is invalid: ${value}`);
|
|
||||||
}
|
|
||||||
return port;
|
|
||||||
}
|
|
||||||
|
|
||||||
function endpointFamily(host) {
|
|
||||||
return host.includes(":") ? "udp6" : "udp4";
|
|
||||||
}
|
|
||||||
|
|
||||||
function endpointKey(host, port) {
|
|
||||||
return `${host}:${port}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatRudpEndpoint(host, port) {
|
|
||||||
return host.includes(":") ? `rudp://[${host}]:${port}` : `rudp://${host}:${port}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildRequestId() {
|
|
||||||
return `mimo-${new Date().toISOString().replace(/[-:.TZ]/g, "")}-${Math.random().toString(36).slice(2, 8)}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
CULTNET_RUDP_PROTOCOL_ID,
|
|
||||||
WEKSA_COMMAND_RUDP_CONNECTION_ID,
|
|
||||||
WEKSA_MIMO_VOICEDESIGN_COMMAND_SCHEMA_ID,
|
|
||||||
WEKSA_MIMO_VOICEDESIGN_RECEIPT_SCHEMA_ID,
|
|
||||||
createWeksaRudpCommandServer,
|
|
||||||
sendWeksaMimoVoiceDesignCommand,
|
|
||||||
formatRudpEndpoint,
|
|
||||||
};
|
|
||||||
|
|
@ -1,148 +0,0 @@
|
||||||
#!/usr/bin/env node
|
|
||||||
import { mkdir, writeFile } from "node:fs/promises";
|
|
||||||
import path from "node:path";
|
|
||||||
|
|
||||||
const document = {
|
|
||||||
schema: "gamecult.eve.provider_advertisement.v1",
|
|
||||||
providerId: "weksa.intent.service",
|
|
||||||
title: "Weksa Intent Service",
|
|
||||||
description:
|
|
||||||
"Weksa converts writing, interlingua, and agent state into typed conversational intent, pronunciation plans, and utterance handoff documents for AquaSynth and other consumers.",
|
|
||||||
mode: "fixture-read-only",
|
|
||||||
serviceVerse: {
|
|
||||||
id: "verse.weksa.service.v1",
|
|
||||||
cultMeshKey: "weksa.service/provider-advertisement",
|
|
||||||
authority: "Weksa daemon identity, schema catalog, and provider publication",
|
|
||||||
owns: [
|
|
||||||
"typed conversational intent",
|
|
||||||
"pronunciation plans",
|
|
||||||
"utterance handoff documents",
|
|
||||||
"lowering traces",
|
|
||||||
"language-project and flavor-profile state",
|
|
||||||
],
|
|
||||||
doesNotOwn: [
|
|
||||||
"AquaSynth learned embeddings",
|
|
||||||
"Faust compilation",
|
|
||||||
"rendered audio",
|
|
||||||
"Persona canonical state",
|
|
||||||
],
|
|
||||||
},
|
|
||||||
nestedVerses: [
|
|
||||||
{
|
|
||||||
id: "weksa.intent",
|
|
||||||
kind: "intent",
|
|
||||||
cultMeshKey: "weksa.intent/current",
|
|
||||||
authority: "Weksa conversational intent compiler",
|
|
||||||
schemas: ["weksa.conversational_intent.v0"],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "weksa.pronunciation",
|
|
||||||
kind: "pronunciation",
|
|
||||||
cultMeshKey: "weksa.pronunciation/current",
|
|
||||||
authority: "Weksa pronunciation lowering",
|
|
||||||
schemas: ["weksa.pronunciation_plan.v0"],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "weksa.utterance",
|
|
||||||
kind: "utterance",
|
|
||||||
cultMeshKey: "weksa.utterance/handoffs",
|
|
||||||
authority: "Weksa utterance handoff export",
|
|
||||||
schemas: ["weksa.utterance_handoff.v0", "weksa.utterance_embedding_handoff.v0.1"],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "weksa.operator",
|
|
||||||
kind: "operator",
|
|
||||||
cultMeshKey: "weksa.operator/status",
|
|
||||||
authority: "Weksa daemon runtime",
|
|
||||||
schemas: ["weksa.operator_state.v0"],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "weksa.cultural_ontology",
|
|
||||||
kind: "cultural_ontology",
|
|
||||||
cultMeshKey: "weksa.cultural-ontology/catalogs",
|
|
||||||
authority: "Weksa cultural ontology profile store",
|
|
||||||
schemas: ["weksa.cultural_ontology_profile.v0", "weksa.cultural_ontology_catalog.v0"],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
schemaWitnessFamilies: [
|
|
||||||
{
|
|
||||||
family: "intent",
|
|
||||||
namespace: "weksa.intent.*",
|
|
||||||
witnesses: ["weksa.conversational_intent.v0"],
|
|
||||||
durableShape: ".weksa/intent/{intentId}.cc",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
family: "pronunciation",
|
|
||||||
namespace: "weksa.pronunciation.*",
|
|
||||||
witnesses: ["weksa.pronunciation_plan.v0"],
|
|
||||||
durableShape: ".weksa/pronunciation/{utteranceId}.cc",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
family: "utterance",
|
|
||||||
namespace: "weksa.utterance.*",
|
|
||||||
witnesses: ["weksa.utterance_handoff.v0", "weksa.utterance_embedding_handoff.v0.1"],
|
|
||||||
durableShape: ".weksa/handoff/{utteranceId}.cc",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
family: "trace",
|
|
||||||
namespace: "weksa.trace.*",
|
|
||||||
witnesses: ["weksa.lowering_trace.v0"],
|
|
||||||
durableShape: ".weksa/traces/{traceId}.cc",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
family: "cultural_ontology",
|
|
||||||
namespace: "weksa.cultural_ontology.*",
|
|
||||||
witnesses: ["weksa.cultural_ontology_profile.v0", "weksa.cultural_ontology_catalog.v0"],
|
|
||||||
durableShape: ".weksa/cultural-ontology/{storeId}.cc",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
commandBoundaries: [
|
|
||||||
{
|
|
||||||
id: "weksa.command.intent.create",
|
|
||||||
owner: "Weksa",
|
|
||||||
acceptedByFixture: false,
|
|
||||||
allowedInputs: ["writing", "interlingua packet", "Persona/agent state projection"],
|
|
||||||
emits: ["weksa.conversational_intent.v0", "weksa.lowering_trace.v0"],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "weksa.command.utterance.lower",
|
|
||||||
owner: "Weksa",
|
|
||||||
acceptedByFixture: false,
|
|
||||||
allowedInputs: ["accepted conversational intent", "language project", "speaker state"],
|
|
||||||
emits: ["weksa.pronunciation_plan.v0", "weksa.utterance_handoff.v0"],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "weksa.command.handoff.export",
|
|
||||||
owner: "Weksa",
|
|
||||||
acceptedByFixture: false,
|
|
||||||
emits: ["weksa.utterance_embedding_handoff.v0.1"],
|
|
||||||
forbiddenTargets: ["Faust compilation", "audio rendering", "AquaSynth synth driver truth"],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
loweringRules: [
|
|
||||||
"Weksa owns conversational intent and utterance lowering.",
|
|
||||||
"AquaSynth owns learned embeddings, synth controls, Faust compilation, and audio output.",
|
|
||||||
"Fixture exports are discovery metadata until the daemon loop commits typed witnesses.",
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
async function main() {
|
|
||||||
const args = process.argv.slice(2);
|
|
||||||
const outIndex = args.indexOf("--out");
|
|
||||||
const outputPath = outIndex >= 0 ? args[outIndex + 1] : undefined;
|
|
||||||
const json = `${JSON.stringify(document, null, 2)}\n`;
|
|
||||||
|
|
||||||
if (outputPath) {
|
|
||||||
const resolved = path.resolve(process.cwd(), outputPath);
|
|
||||||
await mkdir(path.dirname(resolved), { recursive: true });
|
|
||||||
await writeFile(resolved, json, "utf8");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
process.stdout.write(json);
|
|
||||||
}
|
|
||||||
|
|
||||||
main().catch((error) => {
|
|
||||||
console.error(error);
|
|
||||||
process.exitCode = 1;
|
|
||||||
});
|
|
||||||
Loading…
Reference in a new issue