From 70064f487ef3002ecc44a44c60c9f530e6fd3445 Mon Sep 17 00:00:00 2001 From: Sean O'Connor Date: Sat, 21 Mar 2026 19:29:28 -0400 Subject: [PATCH] Fix say_with_emotion with proper NAOqi markup, add transform functions, update seed script for linear branching --- robot-plugins | 2 +- scripts/seed-dev.ts | 143 +++++++++++++++--------------- src/lib/ros/wizard-ros-service.ts | 120 ++++++++++++++++++++----- 3 files changed, 167 insertions(+), 98 deletions(-) diff --git a/robot-plugins b/robot-plugins index 9e0921c..14137ba 160000 --- a/robot-plugins +++ b/robot-plugins @@ -1 +1 @@ -Subproject commit 9e0921c69c36d65c7b72743faa68807361ce766f +Subproject commit 14137ba631de14e6ecaf7fbbf9e76c2219622b25 diff --git a/scripts/seed-dev.ts b/scripts/seed-dev.ts index 96cd435..32401b4 100755 --- a/scripts/seed-dev.ts +++ b/scripts/seed-dev.ts @@ -262,6 +262,35 @@ async function main() { // 5. Create Steps & Actions (The Interactive Storyteller Protocol) console.log("🎬 Creating experiment steps (Interactive Storyteller)..."); + // Pre-create steps that will be referenced before they're defined + // --- Step 5: Story Continues (Convergence point for both branches) --- + const [step5] = await db + .insert(schema.steps) + .values({ + experimentId: experiment!.id, + name: "Story Continues", + description: "Both branches converge here", + type: "robot", + orderIndex: 5, + required: true, + durationEstimate: 15, + }) + .returning(); + + // --- Step 6: Conclusion --- + const [step6] = await db + .insert(schema.steps) + .values({ + experimentId: experiment!.id, + name: "Conclusion", + description: "End the story and thank participant", + type: "robot", + orderIndex: 6, + required: true, + durationEstimate: 25, + }) + .returning(); + // --- Step 1: The Hook --- const [step1] = await db .insert(schema.steps) @@ -363,38 +392,6 @@ async function main() { }, ]); - // --- Step 3: Comprehension Check (Wizard Decision Point) --- - const [step3] = await db - .insert(schema.steps) - .values({ - experimentId: experiment!.id, - name: "Comprehension Check", - description: - "Ask participant about rock color and wait for wizard input", - type: "conditional", - orderIndex: 2, - required: true, - durationEstimate: 30, - conditions: { - variable: "last_wizard_response", - options: [ - { - label: "Correct Response (Red)", - value: "Correct", - nextStepId: step5!.id, - variant: "default", - }, - { - label: "Incorrect Response", - value: "Incorrect", - nextStepId: step5!.id, - variant: "destructive", - }, - ], - }, - }) - .returning(); - // --- Step 4a: Correct Response Branch --- const [step4a] = await db .insert(schema.steps) @@ -423,6 +420,38 @@ async function main() { }) .returning(); + // --- Step 3: Comprehension Check (Wizard Decision Point) --- + const [step3] = await db + .insert(schema.steps) + .values({ + experimentId: experiment!.id, + name: "Comprehension Check", + description: + "Ask participant about rock color and wait for wizard input", + type: "conditional", + orderIndex: 2, + required: true, + durationEstimate: 30, + conditions: { + variable: "last_wizard_response", + options: [ + { + label: "Correct Response (Red)", + value: "Correct", + nextStepId: step4a!.id, + variant: "default", + }, + { + label: "Incorrect Response", + value: "Incorrect", + nextStepId: step4b!.id, + variant: "destructive", + }, + ], + }, + }) + .returning(); + await db.insert(schema.actions).values([ { stepId: step3!.id, @@ -440,12 +469,11 @@ async function main() { name: "Wait for Choice", type: "wizard_wait_for_response", orderIndex: 1, - // Define the options that will be presented to the Wizard parameters: { prompt_text: "Did participant answer 'Red' correctly?", options: [ - { label: "Correct", value: "Correct", nextStepId: step5!.id }, - { label: "Incorrect", value: "Incorrect", nextStepId: step5!.id }, + { label: "Correct", value: "Correct", nextStepId: step4a!.id }, + { label: "Incorrect", value: "Incorrect", nextStepId: step4b!.id }, ], }, sourceKind: "core", @@ -551,20 +579,7 @@ async function main() { }, ]); - // --- Step 5: Story Continues (Convergence point for both branches) --- - const [step5] = await db - .insert(schema.steps) - .values({ - experimentId: experiment!.id, - name: "Story Continues", - description: "Both branches converge here", - type: "robot", - orderIndex: 5, - required: true, - durationEstimate: 15, - }) - .returning(); - + // --- Step 5 actions: Story Continues --- await db.insert(schema.actions).values([ { stepId: step5!.id, @@ -584,37 +599,19 @@ async function main() { { stepId: step5!.id, name: "Wave Goodbye", - type: "nao6-ros2.move_arm", + type: "nao6-ros2.wave_goodbye", orderIndex: 1, parameters: { - arm: "right", - shoulder_pitch: 0.5, - shoulder_roll: 0.3, - elbow_yaw: -0.5, - elbow_roll: 0.8, - speed: 0.4, + text: "See you later!", }, pluginId: NAO_PLUGIN_DEF.robotId || "nao6-ros2", pluginVersion: "2.2.0", - category: "movement", + category: "interaction", retryable: true, }, ]); - // --- Step 6: Conclusion --- - const [step6] = await db - .insert(schema.steps) - .values({ - experimentId: experiment!.id, - name: "Conclusion", - description: "End the story and thank participant", - type: "robot", - orderIndex: 6, - required: true, - durationEstimate: 25, - }) - .returning(); - + // --- Step 6 actions: Conclusion --- await db.insert(schema.actions).values([ { stepId: step6!.id, @@ -894,7 +891,7 @@ async function main() { // 7. Pre-create a pending trial for immediate testing console.log("🧪 Creating a pre-seeded pending trial for testing..."); const p001 = insertedParticipants.find((p) => p.participantCode === "P101"); - + const [pendingTrial] = await db .insert(schema.trials) .values({ @@ -904,7 +901,7 @@ async function main() { scheduledAt: new Date(), }) .returning(); - + console.log(` Created pending trial: ${pendingTrial?.id}`); console.log("\n✅ Database seeded successfully!"); diff --git a/src/lib/ros/wizard-ros-service.ts b/src/lib/ros/wizard-ros-service.ts index e20c4b8..c691457 100644 --- a/src/lib/ros/wizard-ros-service.ts +++ b/src/lib/ros/wizard-ros-service.ts @@ -493,35 +493,43 @@ export class WizardRosService extends EventEmitter { case "move_head": case "turn_head": - this.publish("/joint_angles", "naoqi_bridge_msgs/JointAnglesWithSpeed", { - joint_names: ["HeadYaw", "HeadPitch"], - joint_angles: [ - Number(parameters.yaw) || 0, - Number(parameters.pitch) || 0, - ], - speed: Number(parameters.speed) || 0.3, - }); + this.publish( + "/joint_angles", + "naoqi_bridge_msgs/JointAnglesWithSpeed", + { + joint_names: ["HeadYaw", "HeadPitch"], + joint_angles: [ + Number(parameters.yaw) || 0, + Number(parameters.pitch) || 0, + ], + speed: Number(parameters.speed) || 0.3, + }, + ); await new Promise((resolve) => setTimeout(resolve, 1000)); break; case "move_arm": const arm = String(parameters.arm || "right"); const prefix = arm.toLowerCase() === "left" ? "L" : "R"; - this.publish("/joint_angles", "naoqi_bridge_msgs/JointAnglesWithSpeed", { - joint_names: [ - `${prefix}ShoulderPitch`, - `${prefix}ShoulderRoll`, - `${prefix}ElbowYaw`, - `${prefix}ElbowRoll`, - ], - joint_angles: [ - Number(parameters.shoulder_pitch) || 0, - Number(parameters.shoulder_roll) || 0, - Number(parameters.elbow_yaw) || 0, - Number(parameters.elbow_roll) || 0, - ], - speed: Number(parameters.speed) || 0.3, - }); + this.publish( + "/joint_angles", + "naoqi_bridge_msgs/JointAnglesWithSpeed", + { + joint_names: [ + `${prefix}ShoulderPitch`, + `${prefix}ShoulderRoll`, + `${prefix}ElbowYaw`, + `${prefix}ElbowRoll`, + ], + joint_angles: [ + Number(parameters.shoulder_pitch) || 0, + Number(parameters.shoulder_roll) || 0, + Number(parameters.elbow_yaw) || 0, + Number(parameters.elbow_roll) || 0, + ], + speed: Number(parameters.speed) || 0.3, + }, + ); await new Promise((resolve) => setTimeout(resolve, 1000)); break; @@ -533,7 +541,9 @@ export class WizardRosService extends EventEmitter { break; default: - throw new Error(`Unknown action: ${actionId}. Define this action in your robot plugin.`); + throw new Error( + `Unknown action: ${actionId}. Define this action in your robot plugin.`, + ); } } @@ -741,12 +751,74 @@ export class WizardRosService extends EventEmitter { speed: Number(parameters.speed) || 0.2, }; + case "transformToEmotionalSpeech": + return this.transformToEmotionalSpeech(parameters); + default: console.warn(`Unknown transform function: ${transformFn}`); return parameters; } } + /** + * Transform parameters for emotional speech + * NAOqi markup: \rspd=\ + * For animated speech: ^start(animations/Stand/Gestures/...) + */ + private transformToEmotionalSpeech(parameters: Record): { + data: string; + } { + const text = String(parameters.text || "Hello"); + const emotion = String(parameters.emotion || "neutral"); + const speed = Number(parameters.speed || 1.0); + const speedPercent = Math.round(speed * 100); + + let markedText = text; + + switch (emotion) { + case "happy": + markedText = `\\rspd=120\\^start(animations/Stand/Gestures/Happy_4) ${text}`; + break; + case "excited": + markedText = `\\rspd=140\\^start(animations/Stand/Gestures/Enthusiastic_1) ${text}`; + break; + case "sad": + markedText = `\\rspd=80\\vct=80\\${text}`; + break; + case "calm": + markedText = `\\rspd=90\\${text}`; + break; + case "neutral": + default: + markedText = `\\rspd=${speedPercent}\\${text}`; + break; + } + + return { data: markedText }; + } + + /** + * Transform for wave goodbye - animated speech with waving + */ + private transformToWaveGoodbye(parameters: Record): { + data: string; + } { + const text = String(parameters.text || "Goodbye!"); + const markedText = `\\rspd=110\\^start(animations/Stand/Gestures/Hey_1) ${text} ^start(animations/Stand/Gestures/Hey_1)`; + return { data: markedText }; + } + + /** + * Transform for playing animations + */ + private transformToAnimation(parameters: Record): { + data: string; + } { + const animation = String(parameters.animation || "Hey_1"); + const markedText = `^start(animations/Stand/Gestures/${animation})`; + return { data: markedText }; + } + /** * Schedule reconnection attempt */