Skip to main content

Final Project - Working Process

Valerie Jeyeon YONG

Final Project Ideas
#

1. Scentient Machine
#

Concept
#

Scentient Machine is a device that “reads the atmosphere” of a room. It interprets the invisible atmosphere of a space as emotional data.

Using MEMS gas sensors, the system reads the chemical composition of air, including human breath, perfume, humidity, and CO₂ levels, and translates these into emotional states.

ex) When the air feels heavy and warm, it displays: “The air smells tense.”

→ Tuto: Building an Electronic Nose with MEMS Gas Detection Sensor

Emotion Labels and Training Conditions Examples
#

→ Edge Impulse model for smell/‘feeling of the air’

Installation Design
#

Like the interface of an air purifier or weather application. 

Digital display showing live emotional status (ex. Tense – 82% Confidence)

References

Adnose, Adnan Aga - predict the smell of any object using AI

Smeller 2.0, Wolfgang Georgsdorf - deliver complex sequences of smells played in place of music notes.  → Maybe I can treats scent as a medium and transforms invisible chemical data into a performative art piece

2. Apology Jacket
#

Inspiration
#

I’m interested in the kind of simple and humorous interactive works that Matti showed us, which echo the spirit of Chindogu, a playful useless invention inventions born from everyday inconveniences.

Concept
#

This project originates from an experience that everyone has: in crowded places, I often bump into people’s shoulders but can’t possibly apologize to everyone.

Apology Jacket is a wearable device, embedded in the shoulder of a jacket, that automatically apologizes whenever it detects physical contact. → using AI TTS ?

The project explores the automation of social etiquette and the absurd extension of AI assistance into even the smallest human gestures of politeness. We become more and more reliant on machines and AI, and now even to apologize. Apology Jacket exaggerates this dependency by outsourcing an intimate human behavior to an AI that reacts faster and more obediently than we ever could. The result is both comical and unsettling: an endlessly polite jacket, apologizing to the world.

How it works
#

When the wearer bumps into someone, the embedded sensor detects the collision’s intensity. According to the force, the AI system generates and plays an apology with different tone and repetition. From a calm “Sorry” to an anxious stream of “I’m so sorry! Sorry! Sorry!”.


*After reviewing and organizing my ideas, I find the use of an electric nose interesting, but I’m not very satisfied with the concept itself. So for now, I’m considering either simply running the electric nose experiment or moving forward with the Apology Jacket.*


Apology Jacket Working Process
#

I decided to choose Apology Jacket because I’m more confident in it (and it received more reactions from my classmates).

Sensors

  • velostat
  • conductive fabric
  • small audio player
  • amplifier
  • pico 2W
  • jacket

STEP 1 — Collision Detection
#

First Test with Pressure Sensor and Pico 2W

  • Light collision = 400–700
  • Strong collision = 800–1000
int pressureValue;
int thresholdValue = 600;
int thresholdValue2 = 900;

bool hasApologized = false;

void setup() {
  Serial.begin(9600);
}

void loop() {

  // Read pressure sensor (FSR or force sensor)
  pressureValue = analogRead(26);    

  // Collision detected 
  if (pressureValue > thresholdValue) {

    if (!hasApologized) { // only say sorry once per bump

      if (pressureValue < thresholdValue2) {
        Serial.println("Sorry");
      } 
      else if (pressureValue >= thresholdValue2) {
        Serial.println("SORRY SORRY!");
      }

      hasApologized = true;  // prevent repeating
    }
  }

  // RESET when pressure goes back to normal 
  else {
    hasApologized = false;
  }

  // Print readings
  // Serial.print("Pressure: ");
  // Serial.println(pressureValue);

  delay(10);
}

STEP 2 — Add Audio (AI text-to-speech)
#

  • Connecting to a speaker

Add audio

  • Install BackgroundAudio Library

https://www.arduinolibraries.info/libraries/background-audio https://github.com/earlephilhower/BackgroundAudio

  • WebradioMP3PlusWebUI example model → plays an MP3 web radio using HTTPS connectivity. Includes a serial and HTTP WebServer interface to allow the user to change URLs, volumes, and see the ICY metadata.

Text-to-Speech

  • Used SerialSpeak example model which uses speech API to talk when I type to Serial Monitor.

    → I changed to speak words that are inside the code and combined with the pressure sensors.

// SerialSpeak - Earle F. Philhower, III <earlephilhower@yahoo.com>
// Released to the public domain January 2025

// Reads from the serial port and plays what's typed over the output
// asynchronously.  Can queue up work while still speaking.
// Demonstrates dictionary and voice usage

#include <BackgroundAudioSpeech.h>

// Choose the voice you want
#include <libespeak-ng/voice/en_029.h>
#include <libespeak-ng/voice/en_gb_scotland.h>
#include <libespeak-ng/voice/en_gb_x_gbclan.h>
#include <libespeak-ng/voice/en_gb_x_gbcwmd.h>
#include <libespeak-ng/voice/en_gb_x_rp.h>
#include <libespeak-ng/voice/en.h>
#include <libespeak-ng/voice/en_shaw.h>
#include <libespeak-ng/voice/en_us.h>
#include <libespeak-ng/voice/en_us_nyc.h>
BackgroundAudioVoice v[] = {
  voice_en_029, // 0
  voice_en_gb_scotland, //1
  voice_en_gb_x_gbclan, //2
  voice_en_gb_x_gbcwmd, //3
  voice_en, //4
  voice_en_shaw, //5
  voice_en_us, //6
  voice_en_us_nyc //7
};

#include <PWMAudio.h>
PWMAudio audio(0);
BackgroundAudioSpeech BMP(audio);

int pressureValue;
int thresholdValue = 600;
int thresholdValue2 = 900;

bool hasApologized = false;

void setup() {
  Serial.begin(115200);

  // We need to set up a voice before any output
  BMP.setVoice(v[4]);
  BMP.begin();

  delay(10);

  BMP.speak("Hello. I am your Apology Jacket.");
  delay(2000);
}

void loop() {
   // Read pressure sensor (FSR or force sensor)
  pressureValue = analogRead(26);    

  // Collision detected 
  if (pressureValue > thresholdValue) {

    if (!hasApologized) { // only say sorry once per bump

      if (pressureValue < thresholdValue2) {
        BMP.speak("Sorry");
      } 
      else if (pressureValue >= thresholdValue2) {
        BMP.speak("Sorry Sorry Sorry");
      }

      hasApologized = true;  // prevent repeating
    }
  }

  // RESET when pressure goes back to normal 
  else {
    hasApologized = false;
  }

  // Print readings
  // Serial.print("Pressure: ");
  // Serial.println(pressureValue);

  delay(10);
}

STEP 3 — AI Apology Generation
#

  • Run ChatGPT with openai API

https://www.hackster.io/Shilleh/how-to-set-up-chatgpt-on-a-raspberry-pi-pico-w-5977bf 

https://www.youtube.com/watch?v=EAwh4ul-K0g

#include <WiFi.h>
#include <WiFiClientSecure.h>

const char* ssid = "aalto open";
const char* password = "";

const char* apiKey = "my keycode";

// New 2025 API endpoint
const char* host = "api.openai.com";
const int httpsPort = 443;

// Secure client
WiFiClientSecure client;

void setup() {
  Serial.begin(115200);
  delay(1000);

  Serial.println("Connecting to WiFi...");
  WiFi.begin(ssid);

  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }

  Serial.println("\nConnected!");

  // Required for SSL
  client.setInsecure();

  sendPrompt("Give one short, apology sentence to someone who bumped into your shoulder. No introduction, no explanation, no quotes. Output only the sentence.");
}

void loop() {
  // Nothing
}

void sendPrompt(String prompt) {
  Serial.println("\nConnecting to OpenAI...");

  if (!client.connect(host, httpsPort)) {
    Serial.println("Connection failed!");
    return;
  }

  Serial.println("Connected to OpenAI!");

  // JSON for new "responses" endpoint
  String requestBody = "{";
  requestBody += "\"model\": \"gpt-4.1-mini\",";
  requestBody += "\"input\": \"" + prompt + "\"";
  requestBody += "}";

  // Construct HTTP request
  String request = String("POST /v1/responses HTTP/1.1\r\n") + "Host: api.openai.com\r\n" + "Content-Type: application/json\r\n" + "Authorization: Bearer " + String(apiKey) + "\r\n" + "Content-Length: " + requestBody.length() + "\r\n\r\n" + requestBody;

  client.print(request);

  Serial.println("Request sent. Waiting for response...\n");

  String response = "";

  // Wait for the full response
  unsigned long timeout = millis();
  while (millis() - timeout < 5000) {  // wait up to 5 seconds
    while (client.available()) {
      char c = client.read();
      response += c;
      timeout = millis();  // extend timeout while data is still coming
    }
  }

  // Print the whole response for debugging
  Serial.println("===== RAW RESPONSE =====");
  Serial.println(response);
  Serial.println("========================");

  // Extract "text": "...."
  int pos = response.indexOf("\"text\":");
  if (pos != -1) {
    int start = response.indexOf("\"", pos + 7) + 1;
    int end = response.indexOf("\"", start);

    if (start > 0 && end > start) {
      String text = response.substring(start, end);

      Serial.println("\n===== AI MESSAGE =====");
      Serial.println(text);
      Serial.println("======================\n");
    } else {
      Serial.println("ERROR: Could not extract text.");
    }
  } else {
    Serial.println("ERROR: No 'text' field found.");
  }
}

Prompt: Give one short, apology sentence to someone who bumped into your shoulder. No introduction, no explanation, no quotes. Output only the sentence.

Problem: It takes 6 secs to run. (too long)

Solution: During this 6 sec → “I’m calculating blablabla”

  • Combined the AI generating text code and speaking pressure sensor code
  • Add one more sensor for left and right shoulder
#include <WiFi.h>
#include <WiFiClientSecure.h>

// -----------Wifi setup
const char* ssid = "aalto open";
const char* password = "";

const char* apiKey = "my keycode";

// New 2025 API endpoint
const char* host = "api.openai.com";
const int httpsPort = 443;

// Secure client
WiFiClientSecure client;

// -----------Speech setup
#include <BackgroundAudioSpeech.h>

// Choose the voice you want
#include <libespeak-ng/voice/en_029.h>
#include <libespeak-ng/voice/en_gb_scotland.h>
#include <libespeak-ng/voice/en_gb_x_gbclan.h>
#include <libespeak-ng/voice/en_gb_x_gbcwmd.h>
#include <libespeak-ng/voice/en_gb_x_rp.h>
#include <libespeak-ng/voice/en.h>
#include <libespeak-ng/voice/en_shaw.h>
#include <libespeak-ng/voice/en_us.h>
#include <libespeak-ng/voice/en_us_nyc.h>
BackgroundAudioVoice v[] = {
  voice_en_029,          // 0
  voice_en_gb_scotland,  //1
  voice_en_gb_x_gbclan,  //2
  voice_en_gb_x_gbcwmd,  //3
  voice_en,              //4
  voice_en_shaw,         //5
  voice_en_us,           //6
  voice_en_us_nyc        //7
};

#include <PWMAudio.h>
PWMAudio audio(0);  // PWM on GP0 --> amplifier imput
BackgroundAudioSpeech BMP(audio);

// ---------Pressure sensor
int pressureValueLeft;   // For the left shoulder
int pressureValueRight;  // For the right shoulder

int threshold1 = 600;
int threshold2 = 900;

// bool hasApologized = false;

bool apologizedLeft = false;
bool apologizedRight = false;


void setup() {
  Serial.begin(115200);
  delay(1000);

  Serial.println("Connecting to WiFi...");
  WiFi.begin(ssid);

  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }

  Serial.println("\nConnected!");

  // We need to set up a voice before any output
  BMP.setVoice(v[4]);
  BMP.begin();
  delay(10);

  BMP.speak("Hello. I am your Apology Jacket.");
  delay(1000);

  // Required for SSL
  client.setInsecure();
}

void loop() {
  // Read pressure sensor (FSR or force sensor)
  //pressureValue = analogRead(26);
  pressureValueLeft = analogRead(26);
  pressureValueRight = analogRead(27);


  // Collision detected
  // left shoulder
  if (pressureValueLeft > threshold1) {
    if (!apologizedLeft) {
      if (pressureValueLeft < threshold2) {
        sendPrompt("Give one short apology sentence for a light shoulder bump. No introduction, no explanation, no quotes. Output only the sentence.");
      } else {
        sendPrompt("Give a stronger apology sentence to someone who bumped into your shoulder very hard. No introduction, no explanation, no quotes. Output only the sentence.");
      }
      apologizedLeft = true;
    }
    // RESET when pressure goes back to normal
  } else {
    apologizedLeft = false;
  }

  // right shoulder
  if (pressureValueRight > threshold1) {
    if (!apologizedRight) {
      if (pressureValueRight < threshold2) {
        sendPrompt("Give one short apology sentence for a light shoulder bump. No introduction, no explanation, no quotes. Output only the sentence.");
      } else {
        sendPrompt("Give a stronger apology sentence to someone who bumped into your shoulder very hard. No introduction, no explanation, no quotes. Output only the sentence.");
      }
      apologizedRight = true;
    }
    // RESET when pressure goes back to normal
  } else {
    apologizedRight = false;
  }

  // Print readings
  // Serial.print("Pressure: ");
  // Serial.println(pressureValue);

  delay(10);
}

void sendPrompt(String prompt) {
  Serial.println("\nConnecting to OpenAI...");

  if (!client.connect(host, httpsPort)) {
    Serial.println("Connection failed!");
    return;
  }

  Serial.println("Connected to OpenAI!");

  // JSON for new "responses" endpoint
  String requestBody = "{";
  requestBody += "\"model\": \"gpt-4.1-mini\",";
  requestBody += "\"input\": \"" + prompt + "\"";
  requestBody += "}";

  // Construct HTTP request
  String request = String("POST /v1/responses HTTP/1.1\r\n") + "Host: api.openai.com\r\n" + "Content-Type: application/json\r\n" + "Authorization: Bearer " + String(apiKey) + "\r\n" + "Content-Length: " + requestBody.length() + "\r\n\r\n" + requestBody;

  client.print(request);

  Serial.println("Request sent. Waiting for response...\n");

  String response = "";

  // Speak thinking message
  BMP.speak("One moment please, I’m thinking about the best way to apologize properly.");

  // Wait for the full response
  unsigned long timeout = millis();
  while (millis() - timeout < 3000) {  // wait up to 3 seconds
    while (client.available()) {
      char c = client.read();
      response += c;
      timeout = millis();  // extend timeout while data is still coming
    }
  }

  // Print the whole response for debugging
  Serial.println("===== RAW RESPONSE =====");
  Serial.println(response);
  Serial.println("========================");

  // Extract "text": "...."
  int pos = response.indexOf("\"text\":");
  if (pos != -1) {
    int start = response.indexOf("\"", pos + 7) + 1;
    int end = response.indexOf("\"", start);

    if (start > 0 && end > start) {
      String text = response.substring(start, end);

      Serial.println("\n===== AI MESSAGE =====");
      Serial.println(text);
      Serial.println("======================\n");

      // Speak the result
      while (!BMP.done()) {
        delay(10);
      }
      //BMP.speak(text);
      BMP.speak(cleanUnicode(text));
      return;

    } else {
      Serial.println("ERROR: Could not extract text.");
    }
  } else {
    Serial.println("ERROR: No 'text' field found.");
  }
}

// Convert Unicode escapes into normal ASCII before speaking
String cleanUnicode(String s) {
  s.replace("\\u2019", "'");    // apostrophe
  s.replace("\\u2018", "'");    // left quote
  s.replace("\\u201C", "\"");   // left double quote
  s.replace("\\u201D", "\"");   // right double quote
  s.replace("\\u2026", "...");  // ellipsis
  s.replace("\\u2014", "-");    // em dash

  // Remove any unknown \uXXXX patterns
  int index;
  while ((index = s.indexOf("\\u")) != -1) {
    s.remove(index, 6);  // remove \uXXXX (6 chars)
  }

  return s;
}

STEP 4 —  Making Conductive Fabric Pressure Sensor
#

Tuto https://www.instructables.com/Flexible-Fabric-Pressure-Sensor/ 

  • velostat
  • conductive fabric
  • fabric

Testing:

Making my own pressure sensor:

Problem1: The initial pressure value is too hight (before: 100, now: 800)

Solution: Change the resistor to 120 Ω. Remove the thread from the conductive fabric because it was pressing the sensor against the fabric.

Problem 2: Detect collision even if I move my arm

Solution: Detect the difference between current and previous readings, not just the raw value. (sudden fast increase in pressure):

int lastPressureValue = 0;

int thresholdValue = 800; 
int thresholdValue2 = 900;     // base threshold
int spikeThreshold = 100;      // how fast the value rises

bool hasApologized = false;

void setup() {
  Serial.begin(9600);
}

void loop() {
  int raw = analogRead(26);
  int delta = raw - lastPressureValue;   // detect sudden spikes
 
  // TRUE collision = pressure + fast spike
  if (raw > thresholdValue && delta > spikeThreshold) {

    if (!hasApologized) {
      //Serial.println("REAL COLLISION DETECTED!");
      if (raw < thresholdValue2) Serial.println("Sorry");
      else Serial.println("SORRY SORRY!");
      hasApologized = true;
    }

  } else if (raw < thresholdValue - 50) {
    // Reset only when pressure fully drops
    hasApologized = false;
  }

  // Print readings
  // Serial.print("Pressure: ");
  // Serial.println(lastPressureValue);

  lastPressureValue = raw;
  delay(10);
}

STEP 5 — Integration Into Jacket
#

  • Sew the pressure sensors on the shoulder

  • Make my own breadboard

  • Wiring inside fabric

Speaker inside a pocket

  • Battery → power bank

Final Result
#

Final code
#

#include <WiFi.h>
#include <WiFiClientSecure.h>

// -----------Wifi setup
const char* ssid = "aalto open";
const char* password = "";

const char* apiKey = "my keycode";

// New 2025 API endpoint
const char* host = "api.openai.com";
const int httpsPort = 443;

// Secure client
WiFiClientSecure client;

// -----------Speech setup
#include <BackgroundAudioSpeech.h>

// Choose the voice you want
#include <libespeak-ng/voice/en_029.h>
#include <libespeak-ng/voice/en_gb_scotland.h>
#include <libespeak-ng/voice/en_gb_x_gbclan.h>
#include <libespeak-ng/voice/en_gb_x_gbcwmd.h>
#include <libespeak-ng/voice/en_gb_x_rp.h>
#include <libespeak-ng/voice/en.h>
#include <libespeak-ng/voice/en_shaw.h>
#include <libespeak-ng/voice/en_us.h>
#include <libespeak-ng/voice/en_us_nyc.h>
BackgroundAudioVoice v[] = {
  voice_en_029,          // 0
  voice_en_gb_scotland,  //1
  voice_en_gb_x_gbclan,  //2
  voice_en_gb_x_gbcwmd,  //3
  voice_en,              //4
  voice_en_shaw,         //5
  voice_en_us,           //6
  voice_en_us_nyc        //7
};

#include <PWMAudio.h>
PWMAudio audio(0);  // PWM on GP0 --> amplifier imput
BackgroundAudioSpeech BMP(audio);

// ---------Pressure sensor
int lastPressureLeft = 0;
int lastPressureRight = 0;

int pressureValueLeft;   // For the left shoulder
int pressureValueRight;  // For the right shoulder

int threshold1 = 800;
int threshold2 = 900;

int spikeThreshold = 100;      // how fast the value rises

// bool hasApologized = false;

bool apologizedLeft = false;
bool apologizedRight = false;


void setup() {
  Serial.begin(115200);
  delay(1000);

  Serial.println("Connecting to WiFi...");
  WiFi.begin(ssid);

  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }

  Serial.println("\nConnected!");

  // We need to set up a voice before any output
  BMP.setVoice(v[4]);
  BMP.begin();
  delay(10);

  BMP.speak("Hello. I am your Apology Jacket.");
  delay(2000);

  // Required for SSL
  client.setInsecure();
}

void loop() {
  // Read pressure sensor (FSR or force sensor)
  pressureValueLeft = analogRead(27);
  pressureValueRight = analogRead(26);

  // Collision detected
  // --- Left shoulder ---
  int deltaLeft = pressureValueLeft - lastPressureLeft;   // detect sudden spikes
  
  if (pressureValueLeft > threshold1 && deltaLeft > spikeThreshold) {

    if (!apologizedLeft) {
      //Serial.println("REAL COLLISION DETECTED!");
      if (pressureValueLeft < threshold2) 
        sendPrompt("Give one short apology sentence for a light shoulder bump. No introduction, no explanation, no quotes. Output only the sentence.");
      else 
        sendPrompt("Give a stronger apology sentence to someone who bumped into your shoulder very hard. No introduction, no explanation, no quotes. Output only the sentence.");
      apologizedLeft = true;
    }

  } else if (pressureValueLeft < threshold1 - 50) {
    // Reset only when pressure fully drops
    apologizedLeft = false;
  }
  lastPressureLeft = pressureValueLeft;

  // --- Right shoulder ---
  int deltaRight = pressureValueRight - lastPressureRight;   // detect sudden spikes
  
  if (pressureValueRight > threshold1 && deltaRight > spikeThreshold) {

    if (!apologizedRight) {
      //Serial.println("REAL COLLISION DETECTED!");
      if (pressureValueRight < threshold2) 
        sendPrompt("Give one short apology sentence for a light shoulder bump. No introduction, no explanation, no quotes. Output only the sentence.");
      else 
        sendPrompt("Give a stronger apology sentence to someone who bumped into your shoulder very hard. No introduction, no explanation, no quotes. Output only the sentence.");
      apologizedRight = true;
    }

  } else if (pressureValueRight < threshold1 - 50) {
    // Reset only when pressure fully drops
    apologizedRight = false;
  }
  lastPressureRight = pressureValueRight;
  
  // Print readings
  // Serial.print("Pressure: ");
  // Serial.println(pressureValue);

  delay(10);
}

void sendPrompt(String prompt) {
  Serial.println("\nConnecting to OpenAI...");

  if (!client.connect(host, httpsPort)) {
    Serial.println("Connection failed!");
    return;
  }

  Serial.println("Connected to OpenAI!");

  // JSON for new "responses" endpoint
  String requestBody = "{";
  requestBody += "\"model\": \"gpt-4.1-mini\",";
  requestBody += "\"input\": \"" + prompt + "\"";
  requestBody += "}";

  // Construct HTTP request
  String request = String("POST /v1/responses HTTP/1.1\r\n") + "Host: api.openai.com\r\n" + "Content-Type: application/json\r\n" + "Authorization: Bearer " + String(apiKey) + "\r\n" + "Content-Length: " + requestBody.length() + "\r\n\r\n" + requestBody;

  client.print(request);

  Serial.println("Request sent. Waiting for response...\n");

  String response = "";

  // Speak thinking message
  BMP.speak("One moment please, I’m thinking about the best way to apologize properly.");

  // Wait for the full response
  unsigned long timeout = millis();
  while (millis() - timeout < 3000) {  // wait up to 3 seconds
    while (client.available()) {
      char c = client.read();
      response += c;
      timeout = millis();  // extend timeout while data is still coming
    }
  }

  // Print the whole response for debugging
  Serial.println("===== RAW RESPONSE =====");
  Serial.println(response);
  Serial.println("========================");

  // Extract "text": "...."
  int pos = response.indexOf("\"text\":");
  if (pos != -1) {
    int start = response.indexOf("\"", pos + 7) + 1;
    int end = response.indexOf("\"", start);

    if (start > 0 && end > start) {
      String text = response.substring(start, end);

      Serial.println("\n===== AI MESSAGE =====");
      Serial.println(text);
      Serial.println("======================\n");

      // Speak the result
      while (!BMP.done()) {
        delay(10);
      }
      //BMP.speak(text);
      BMP.speak(cleanUnicode(text));
      return;
      delay(1000);

    } else {
      Serial.println("ERROR: Could not extract text.");
    }
  } else {
    Serial.println("ERROR: No 'text' field found.");
  }
}

// Convert Unicode escapes into normal ASCII before speaking
String cleanUnicode(String s) {
  s.replace("\\u2019", "'");    // apostrophe
  s.replace("\\u2018", "'");    // left quote
  s.replace("\\u201C", "\"");   // left double quote
  s.replace("\\u201D", "\"");   // right double quote
  s.replace("\\u2026", "...");  // ellipsis
  s.replace("\\u2014", "-");    // em dash

  // Remove any unknown \uXXXX patterns
  int index;
  while ((index = s.indexOf("\\u")) != -1) {
    s.remove(index, 6);  // remove \uXXXX (6 chars)
  }

  return s;
}