Emotion balancer - POC

by morphcast

HTML

<script src="https://ai-sdk.morphcast.com/stage/ai-sdk.js"></script>
<div id="app">
Emotion balancer - POC v3.2
<br />
<button type="button" :disabled="!!started" v-on:click="started=true;init();">StartSDK!</button>
<button type="button" v-on:click="toggleRaw();">Show {{ raw ? 'balanced' : 'raw' }} output</button>
<button type="button" v-on:click="reset();">Reset</button>
<br />
Output is {{ raw ? 'raw' : 'balanced (operating at ' + Math.round(getWindowUsage*100) + ' %)' }}. Used last {{ getIterations }} frames for training.
<div class="wrapper" :class="{black:black}">
  <div class="centered">
    <div class="emo angry">
      <span>Angry</span>
      <div class="level">
        <div class="lvl" :style="{height : level.angry+'%'}"></div>
      </div>
    </div>
    <div class="emo disgust">
      <span>Disgust</span>
      <div class="level">
        <div class="lvl" :style="{height : level.disgust+'%'}"></div>
      </div>
    </div>
    <div class="emo fear">
      <span>Fear</span>
      <div class="level">
        <div class="lvl" :style="{height : level.fear+'%'}"></div>
      </div>
    </div>
    <div class="emo happy">
      <span>Happy</span>
      <div class="level">
        <div class="lvl" :style="{height : level.happy+'%'}"></div>
      </div>
    </div>
    <div class="emo sad">
      <span>Sad</span>
      <div class="level">
        <div class="lvl" :style="{height : level.sad+'%'}"></div>
      </div>
    </div>
    <div class="emo surprise">
      <span>Surprise</span>
      <div class="level">
        <div class="lvl" :style="{height : level.surprise+'%'}"></div>
      </div>
    </div>
    <div class="emo neutral">
      <span>Neutral</span>
      <div class="level">
        <div class="lvl" :style="{height : level.neutral+'%'}"></div>
      </div>
    </div>
  </div>
</div>
</div>

CSS

#app {
      height: 100%;
      width: 100%;
    }

    .centered {
      position: absolute;
      top: 0;
      left: 0;
      right: 0;
      bottom: 0;
      margin: auto;
      max-width: 400px;
      display: flex;
      justify-content: space-evenly;
      height: calc(100% - 150px);
    }

    .emo {
      width: 40px;
      height: 100%;
      flex: 0 0 40px;
    }

    .emo span {
      display: block;
      width: 100%;
      text-align: center;
      color: black;
      font-size: 13px;
      font-weight: 300;
    }

    .emo .level {
      width: 6px;
      margin: 10px 17px;
      background-color: darkgray;
      position: relative;
      height: 80%
    }

    .emo .level .lvl {
      position: absolute;
      width: 100%;
      bottom: 0;

      transition: height 200ms;
    }

    .emo.angry .lvl {
      background-color: #e60f28;
    }

    .emo.disgust .lvl {
      background-color: #27d534;
    }

    .emo.fear .lvl {
      background-color: #8b4f92;
    }

    .emo.happy .lvl {
      background-color: #ffea00;
    }

    .emo.sad .lvl {
      background-color: #64b5e6;
    }

    .emo.surprise .lvl {
      background-color: #ffbbc7;
    }

    .emo.neutral .lvl {
      background-color: #3e3e3e;
    }

    .emo img {
      width: 28px;
      margin: 0 6px;
    }

    .black .emo span {
      color: white;
    }

    @media screen and (max-width: 768px) {
      .centered {
        height: calc(100% - 150px);
      }

      .emo .level {
        margin: 5px 17px;
      }
    }

JavaScript

///// POC

class RunningAvgBalance {
  constructor(maxWindowSize = 100) {
    this._avgEmotions = {
      Angry: 0,
      Disgust: 0,
      Fear: 0,
      Happy: 0,
      Sad: 0,
      Surprise: 0
    };
    this._window = {
      Angry: [],
      Disgust: [],
      Fear: [],
      Happy: [],
      Sad: [],
      Surprise: []
    };
    this._hasReachedMaxSize = () => this.getWindowSize() >= maxWindowSize;
    this._windowUsage = () => this.getWindowSize() / maxWindowSize;
  }
  
  getWindowUsage() {
  	return this._windowUsage();
  }
  
  
  getWindowSize() {
  	return this._window.Angry.length;
  }
  
  canTrain() {
  	return true;
  }

  _pushAvg(newEmotions) {
    Object.keys(this._avgEmotions).forEach((key) => {
      const n = this.getWindowSize();
    	const new_value = newEmotions[key];
      const old_avg = this._avgEmotions[key];
      
      if ( this._hasReachedMaxSize() ) {
      	const removed_value = this._window[key].shift();
        this._avgEmotions[key] = (old_avg*n-removed_value+new_value)/n;
      } else {
      	this._avgEmotions[key] = (n * old_avg + new_value) / (n+1);
      }
      this._window[key].push(new_value);
    });
  }

  next(emo) {
    if (this.canTrain()) {
      this._pushAvg(emo);
      console.log("training...", this._avgEmotions);
    }

    let avg = this._avgEmotions;
    const subtractOffset = (x, offset) => (x > offset) ? x - offset : 0;
    const gain = (offset) => 1/(Math.max(1-offset,0.001));

    let ret = {};
    Object.keys(avg).forEach((k) => {
    	const dynamic_offset = avg[k] * this._windowUsage();
      ret[k] = subtractOffset(emo[k], dynamic_offset)*gain(dynamic_offset);
    });
    ret.Neutral = emo.Neutral;// + sum;
    let newSum = Object.values(ret).reduce((a,b)=>a+b, 0);
    Object.keys(ret).forEach((k) => {
      ret[k] *= 1/newSum;
    });
    return ret;
  }


}


/////////////// script

let startSDK;
CY.loader().addModule(CY.modules().FACE_EMOTION.name, {smoothness: 0.5})
          .delayMs(142)
  ...