Fix say_with_emotion with proper NAOqi markup, add transform functions, update seed script for linear branching

This commit is contained in:
2026-03-21 19:29:28 -04:00
parent 91d03a789d
commit 70064f487e
3 changed files with 167 additions and 98 deletions

View File

@@ -262,6 +262,35 @@ async function main() {
// 5. Create Steps & Actions (The Interactive Storyteller Protocol) // 5. Create Steps & Actions (The Interactive Storyteller Protocol)
console.log("🎬 Creating experiment steps (Interactive Storyteller)..."); 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 --- // --- Step 1: The Hook ---
const [step1] = await db const [step1] = await db
.insert(schema.steps) .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 --- // --- Step 4a: Correct Response Branch ---
const [step4a] = await db const [step4a] = await db
.insert(schema.steps) .insert(schema.steps)
@@ -423,6 +420,38 @@ async function main() {
}) })
.returning(); .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([ await db.insert(schema.actions).values([
{ {
stepId: step3!.id, stepId: step3!.id,
@@ -440,12 +469,11 @@ async function main() {
name: "Wait for Choice", name: "Wait for Choice",
type: "wizard_wait_for_response", type: "wizard_wait_for_response",
orderIndex: 1, orderIndex: 1,
// Define the options that will be presented to the Wizard
parameters: { parameters: {
prompt_text: "Did participant answer 'Red' correctly?", prompt_text: "Did participant answer 'Red' correctly?",
options: [ options: [
{ label: "Correct", value: "Correct", nextStepId: step5!.id }, { label: "Correct", value: "Correct", nextStepId: step4a!.id },
{ label: "Incorrect", value: "Incorrect", nextStepId: step5!.id }, { label: "Incorrect", value: "Incorrect", nextStepId: step4b!.id },
], ],
}, },
sourceKind: "core", sourceKind: "core",
@@ -551,20 +579,7 @@ async function main() {
}, },
]); ]);
// --- Step 5: Story Continues (Convergence point for both branches) --- // --- Step 5 actions: Story Continues ---
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();
await db.insert(schema.actions).values([ await db.insert(schema.actions).values([
{ {
stepId: step5!.id, stepId: step5!.id,
@@ -584,37 +599,19 @@ async function main() {
{ {
stepId: step5!.id, stepId: step5!.id,
name: "Wave Goodbye", name: "Wave Goodbye",
type: "nao6-ros2.move_arm", type: "nao6-ros2.wave_goodbye",
orderIndex: 1, orderIndex: 1,
parameters: { parameters: {
arm: "right", text: "See you later!",
shoulder_pitch: 0.5,
shoulder_roll: 0.3,
elbow_yaw: -0.5,
elbow_roll: 0.8,
speed: 0.4,
}, },
pluginId: NAO_PLUGIN_DEF.robotId || "nao6-ros2", pluginId: NAO_PLUGIN_DEF.robotId || "nao6-ros2",
pluginVersion: "2.2.0", pluginVersion: "2.2.0",
category: "movement", category: "interaction",
retryable: true, retryable: true,
}, },
]); ]);
// --- Step 6: Conclusion --- // --- Step 6 actions: 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();
await db.insert(schema.actions).values([ await db.insert(schema.actions).values([
{ {
stepId: step6!.id, stepId: step6!.id,

View File

@@ -493,35 +493,43 @@ export class WizardRosService extends EventEmitter {
case "move_head": case "move_head":
case "turn_head": case "turn_head":
this.publish("/joint_angles", "naoqi_bridge_msgs/JointAnglesWithSpeed", { this.publish(
joint_names: ["HeadYaw", "HeadPitch"], "/joint_angles",
joint_angles: [ "naoqi_bridge_msgs/JointAnglesWithSpeed",
Number(parameters.yaw) || 0, {
Number(parameters.pitch) || 0, joint_names: ["HeadYaw", "HeadPitch"],
], joint_angles: [
speed: Number(parameters.speed) || 0.3, Number(parameters.yaw) || 0,
}); Number(parameters.pitch) || 0,
],
speed: Number(parameters.speed) || 0.3,
},
);
await new Promise((resolve) => setTimeout(resolve, 1000)); await new Promise((resolve) => setTimeout(resolve, 1000));
break; break;
case "move_arm": case "move_arm":
const arm = String(parameters.arm || "right"); const arm = String(parameters.arm || "right");
const prefix = arm.toLowerCase() === "left" ? "L" : "R"; const prefix = arm.toLowerCase() === "left" ? "L" : "R";
this.publish("/joint_angles", "naoqi_bridge_msgs/JointAnglesWithSpeed", { this.publish(
joint_names: [ "/joint_angles",
`${prefix}ShoulderPitch`, "naoqi_bridge_msgs/JointAnglesWithSpeed",
`${prefix}ShoulderRoll`, {
`${prefix}ElbowYaw`, joint_names: [
`${prefix}ElbowRoll`, `${prefix}ShoulderPitch`,
], `${prefix}ShoulderRoll`,
joint_angles: [ `${prefix}ElbowYaw`,
Number(parameters.shoulder_pitch) || 0, `${prefix}ElbowRoll`,
Number(parameters.shoulder_roll) || 0, ],
Number(parameters.elbow_yaw) || 0, joint_angles: [
Number(parameters.elbow_roll) || 0, Number(parameters.shoulder_pitch) || 0,
], Number(parameters.shoulder_roll) || 0,
speed: Number(parameters.speed) || 0.3, Number(parameters.elbow_yaw) || 0,
}); Number(parameters.elbow_roll) || 0,
],
speed: Number(parameters.speed) || 0.3,
},
);
await new Promise((resolve) => setTimeout(resolve, 1000)); await new Promise((resolve) => setTimeout(resolve, 1000));
break; break;
@@ -533,7 +541,9 @@ export class WizardRosService extends EventEmitter {
break; break;
default: 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, speed: Number(parameters.speed) || 0.2,
}; };
case "transformToEmotionalSpeech":
return this.transformToEmotionalSpeech(parameters);
default: default:
console.warn(`Unknown transform function: ${transformFn}`); console.warn(`Unknown transform function: ${transformFn}`);
return parameters; return parameters;
} }
} }
/**
* Transform parameters for emotional speech
* NAOqi markup: \rspd=<speed>\<text>
* For animated speech: ^start(animations/Stand/Gestures/...)
*/
private transformToEmotionalSpeech(parameters: Record<string, unknown>): {
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<string, unknown>): {
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<string, unknown>): {
data: string;
} {
const animation = String(parameters.animation || "Hey_1");
const markedText = `^start(animations/Stand/Gestures/${animation})`;
return { data: markedText };
}
/** /**
* Schedule reconnection attempt * Schedule reconnection attempt
*/ */