Hazar Ekin Uçan

Selected work

VR Prosodic Speech Analysis System

A completed system that measures how a person speaks inside a VR exercise, normalised against their own calm and urgent references.

Role
Python engine and API
Context
Personal project
Period
2026
Stack
Python · FastAPI · Faster-Whisper · Praat / parselmouth · librosa

What it is A system that measures vocal arousal during a VR exercise and shows a personally normalised score in real time.

What I built The Python analysis engine and the FastAPI service the VR layer drives.

My role

I designed and wrote the Python analysis engine: the recording layer, the Faster-Whisper transcription and word timing, the prosodic feature extraction built on Praat and librosa, the two-anchor personal calibration model, the scoring, the quality gates and the session logging. I also wrote the FastAPI service that the VR layer calls. Every commit in the repository is mine. The system runs end to end on a single Windows PC with the headset connected over Quest Link.

01

Measuring how someone speaks

Inside a VR exercise, the way someone talks carries information the task score does not capture.

If someone is working through a simulated procedure in VR, the system already knows whether they completed the steps. It knows nothing about how they sounded doing it. Speech carries that: pitch, loudness, tempo, where the pauses fall. I wanted to turn that into a number while the exercise is still running.

A fixed threshold does not work. If I say that anything above 180 Hz or 70 dB counts as elevated, the system ends up measuring the person instead of their state. A naturally loud speaker crosses that line while calm. A quiet one never crosses it at all, even under real pressure. Any absolute acoustic threshold is a statement about voices in general, and what I wanted to detect was a change in one voice.

  • Hardware One Windows PC, Meta Quest 3 over Quest Link, audio from the Oculus virtual microphone at 16 kHz mono
  • Interaction One controller button: hold to record, release to analyse
  • Boundary No audio analysis reimplemented in C#. One implementation, in Python
  • Latency Analysis returns while the exercise is still running
  • Discipline Return nothing before returning a bad score

02

Two anchors instead of one threshold

Every speaker gets their own scale, with both ends measured from their own voice.

Before the exercise, each user records two short sentences. The first is spoken normally and becomes their baseline. The second is spoken with urgency and becomes their upperline. For each feature, the distance between those two recordings is that person’s own range.

Everything after that is measured as progress along that range: zero at the baseline, one at the upperline. A loud speaker and a quiet speaker who have each moved the same proportion of their own way toward urgency get the same score.

Diagram: personal calibration

Speaker A

louder, higher pitched

Baseline 112 Hz 0

Upperline 196 Hz 100

Speaker B

quieter, lower pitched

Baseline 78 Hz 0

Upperline 121 Hz 100

Signed gap

slower under urgency

Baseline 5.4 syll/s 0

Upperline 4.1 syll/s 100

Each speaker's own calm baseline and high-urgency upperline become the ends of their own scale, so the score reports movement along a personal range rather than an absolute level. The two speakers above sit at different absolute pitches and reach the same score. The third axis shows the signed case: where a speaker's upperline falls below their baseline for a feature, the scale simply runs the other way. Values shown are unit labels for the method, not measurements.

The detail I am most pleased with is that the gap is signed. Most people speak faster under urgency, so their articulation rate upperline sits above their baseline. Some speak more slowly and deliberately instead. The formula does not treat that as an error to correct. It keeps whichever direction that person actually showed. The gap is simply negative, and progress along it is computed the same way.

app_speech_integrated.py · compare_with_two_anchors()

raw_progress = (
  current_number - baseline_number
) / gap

# Baseline'ın ters yönündeki değişim panik puanı üretmez.
score_progress = max(raw_progress, 0.0)

# Upperline'ı aşmak mümkündür. 1.25 sınırı, tek özelliğin
# bütün skoru aşırı şişirmesini engeller.
score_progress = min(score_progress, 1.25)

weighted_progress += score_progress * weight
used_weight += weight

Progress along a personal gap. Negative gaps work unchanged; movement the other way scores zero; one feature is capped so it cannot dominate.

Six features carry the score. Each has a weight and a minimum gap. The minimum gap is the smallest separation between a person’s two anchors that still counts as a real difference instead of noise.

The six scored prosodic features, with their weight in the final score and the minimum separation required between a speaker’s two calibration anchors.

FeatureWeightMinimum gapSource
F0 median0.255 HzPraat pitch
Mean intensity0.202 dBPraat intensity
Articulation rate0.200.30 syll/sWhisper word timing
Max intensity0.153 dBPraat intensity
Pitch range (p90−p10)0.105 HzPraat pitch
Pause ratio0.100.03Whisper word timing
One real recording through the engine: the captured waveform, the word boundaries Faster-Whisper returned, and the pitch figures extracted from it. This session was captured on a laptop microphone, not through the headset, and it is a single utterance. It shows what the measurement path produces. It is not a validation of it.

Timing comes from Faster-Whisper’s word-level timestamps instead of energy based voice activity detection. Energy VAD cannot tell a pause from a quiet room, and a noisy headset microphone makes it worse. Word boundaries from the transcript are a much better basis for saying where speech actually stopped. Syllables are approximated by counting Turkish vowels. That is a documented approximation and not a real syllabifier, and it is good enough for comparing a speaker with themselves.

The system is deliberately built in two layers with an HTTP bridge between them.

Diagram: system architecture

VR layer

Quest 3 Quest Link · controller button
Unity scene interaction and result panel
FastAPI /health · /start · /stop

Python analysis engine

  1. 01 Record 16 kHz mono
  2. 02 Transcribe · Faster-Whisper
  3. 03 Extract prosodic features
  4. 04 Quality checks
  5. 05 Personal calibration
  6. 06 Score 0-100

Runs on its own, with no headset attached

Two layers and one bridge. The VR side handles interaction and display; every piece of analysis stays in Python, which avoids reimplementing audio libraries in C# and keeps one authoritative implementation of the measurement. The stages inside the engine run in order on each recording.

Keeping every measurement in Python is what makes that split worth having. Praat, librosa and Faster-Whisper have no equivalent in C#. Porting a subset of them into the VR project would have created a second implementation of the measurement, and I would have had to keep the two in agreement.

03

Making the score refuse to lie

Most of the engineering here is in the paths that return nothing.

A number on a panel gets believed. That is a good reason to be careful about when one is produced at all. The gates below are the part of this system I would most want to be judged on.

Diagram: scoring and quality gates

  1. 01

    Recording usable?

    Clipping at peak ≥ 0.99, too quiet below 0.02, voiced frames under 20%

    Warn and ask for the recording again

  2. 02

    Calibration phrase spoken?

    Transcript compared against the expected sentence

    Repeat the anchor

  3. 03

    Did this feature separate?

    Baseline and upperline must differ by the feature’s minimum gap

    Drop the feature from scoring

  4. 04

    Enough of the profile left?

    At least two surviving features and 0.40 combined weight

    Reject the calibration

If every gate passes

  • Progress along each personal gap, signed so the direction is the speaker’s own
  • Movement opposite to that direction contributes zero
  • A single feature is capped at 1.25 so it cannot dominate
  • Weighted, then clamped to 0-100
Four gates stand between a recording and a score, and each has an explicit exit. The system returns nothing before it returns a number it cannot justify. A dropped feature, a rejected calibration and a repeated anchor are all normal outcomes.

The feature level gate is the one that matters most. If a person’s baseline and upperline recordings did not separate on a given feature, that feature carries no information about them. Maybe they spoke at the same pitch both times. Including it would add noise dressed as signal. So it is dropped, and the score is computed from the features that did separate, reweighted.

That can cascade. Drop enough features and there is not enough profile left to score anything. Below two surviving features, or 0.40 of combined weight, calibration is rejected outright and the user records the anchors again.

Recording quality is checked before any of that. Clipping at a peak of 0.99 or above, a peak below 0.02, or fewer than 20% voiced frames all raise a warning. There is a subtler one too. If the voice activity detector reports that 98% or more of the window is active speech, that is usually a noisy room being heard as continuous talking, and the timing features derived from it are not trustworthy.

baseline_manager.py · evaluate_recording_quality()

if speech_window > 0:
  active_ratio = active_duration / speech_window

  if active_ratio >= 0.98:
      warnings.append(
          "Kayıdın neredeyse tamamı aktif konuşma olarak algılandı. "
          "Gürültü nedeniyle VAD/sessizlik tespiti güvenilir olmayabilir."
      )

The room, not the speaker, is what this check is looking for.

There is one more check I like. The calibration phrase itself is verified: the transcript of what the person said is compared against the sentence they were asked to say. A baseline recorded while someone was talking to the person next to them should not silently become the reference every later score is measured against.

04

The finished system

Button to score, end to end, on one machine.

The system, end to end

  1. Captures audio from the Quest microphone over Quest Link
  2. Controller button drives /start and /stop on the FastAPI bridge
  3. Returns transcript and word-level timing from Faster-Whisper
  4. Extracts six prosodic features per recording: F0 median and range, mean and max intensity, articulation rate, pause ratio
  5. Builds a personal calibration profile from the baseline and upperline anchors, and validates it before use
  6. Returns a 0-100 personally normalised score to the VR panel
  7. Writes per-utterance JSON, a session CSV and a session report for later analysis
Three photographs: working on the analysis code at a laptop, the Quest 3 headset being worn with the Unity scene on a wall display, and the project poster presented in the laboratory.
The parts that do not appear in a code listing: the analysis engine being worked on, the headset in use with the Unity scene running, and the project presented. The example figures visible on the poster are illustrative, not measured results.

The engine runs without the headset attached. That is how I developed it and how it stays testable: a WAV file and a phrase are enough to exercise the whole measurement path. The VR layer only ever triggers a recording and displays what comes back.

Where the limits are

A prosodic score says how far someone's speech has moved along their own range between calm and urgent. It does not say why. Tiredness, a cold, a raised voice across a noisy room and genuine urgency all push the same features in the same direction, and nothing in the signal separates them. The score is explicitly not a clinical measure. The code says so where it prints a result, and I would keep saying it anywhere this was used. The syllable count behind the rate features is a vowel-count approximation for Turkish, which is good enough to compare a speaker against themselves and not good enough to compare speakers against each other.

Evidence on this page: Source code · Architecture · System design

Open to conversations about robotics, automation and software for physical systems.