UX session analysis
Moderated sessions become a corpus you can question. The transcript is joined to what was on screen at that second, so one query reaches every session at once and returns the moment rather than the recording.
Applies empty and reads Three to five sessions from ONE study. Connect your own data before querying.
- word-timed words joined to screen states
- 7,257 / 319
- median scene, cut at screen transitions
- 5s
- claim moments, setting changes, task boundaries
- 112 / 22 / 9
- sessions where hand-off-wheel timing works
- 2 of 6
- click to first answer
- 6.3 s
How these were measured
- 7,257 / 319 word-timed words joined to screen states: Measured on the reference corpus of six sessions. The join is the product: a word without the screen it was spoken over is a transcript, and a screen state without the word is a screenshot.
- 5s median scene, cut at screen transitions: Segmenting on the display changing rather than on a fixed interval is what makes a result land on a behaviour instead of in the middle of one.
- 112 / 22 / 9 claim moments, setting changes, task boundaries: From the same six sessions: 112 moments where a participant made a claim, 22 setting changes with durations read off the display rather than from a knob sensor, and 9 task boundaries taken from the moderator narrating the protocol.
- 2 of 6 sessions where hand-off-wheel timing works: 61.5% of frames on one rig and 15.9% on another, agreeing with 7 of 9 hand-labelled frames. It fails on the rest because the camera is framed on the display with the wheel out of shot. This is a rig property, not a model property.
- 6.3 s click to first answer: from clicking apply to the first search that answers, timed on production on 6 November 2026 by the harness that runs nightly
What’s inside
- Extractors
- multimodaltext
- Retrievers
- find-the-moment·feature_searchtask-roll-up·feature_search → group_byfrustration-monitor·feature_search
See it work
- where did people hesitate on the checkout stepfind-the-moment
the word and the screen state at that second, in one result
- what did everyone say about the new navigationtask-roll-up
grouped on participant_id, so no participant is missing from the answer
- the moment someone says they are confusedfind-the-moment
opens at the second it happened rather than at the recording
cleared- session-scenes -> session-transcripts
- participant
- screen state at that second
- route: cleared
How it's wired
Applies 1 buckets, 2 collections and 3 retrievers today
Drag nodes to rearrange. Drag the background to pan, ctrl or cmd plus scroll to zoom. Click a node for its settings.
What it costs
Processing is priced the way a batch is charged: each bucket's minutes, images, pages or tokens times its rate card lines. The batch dry run in Studio gives the binding quote.
Models
- Multilingual E5 Large InstructServed on MixpeekEmbedding
- Vertex AI multimodalembedding@001Provider APIEmbedding
Collections and their documents
Reads video from content
| Field | Type | Required |
|---|---|---|
| document_id | string | set by Mixpeek |
| collection_id | string | set by Mixpeek |
| study_id | string | no |
| participant_id | string | no |
| rig_id | string | no |
| study_task_id | string | no |
| file_location | as sent | no |
| segment_id | as sent | no |
| start_time | as sent | no |
| end_time | as sent | no |
mixpeek://multimodal_extractor@v1/vertex_multimodal_embeddingfrom Vertex AI multimodalembedding@001mixpeek://text_extractor@v1
Reads text from transcription
| Field | Type | Required |
|---|---|---|
| document_id | string | set by Mixpeek |
| collection_id | string | set by Mixpeek |
| study_id | as sent | no |
| participant_id | as sent | no |
| study_task_id | as sent | no |
| segment_id | as sent | no |
| video_segment_url | as sent | no |
| start_time | as sent | no |
| end_time | as sent | no |
mixpeek://multimodal_extractor@v1/vertex_multimodal_embeddingfrom Vertex AI multimodalembedding@001mixpeek://text_extractor@v1
Retrievers
- find-the-momentfeature_search over session-scenes, session-transcripts; input query
- task-roll-upfeature_search then group_by over session-transcripts, session-scenes; input query
- frustration-monitorfeature_search over session-transcripts; input query, document_id
Spend limits
- Daily spend budget in the manifest
- 0.1 USD a day; new batches pause once it is spent
- Acceptance run ceiling
- 0.1 USD
- Model cost
- Unmeasured for 2 of 2 models
Monthly cost at your volume is in the calculator. Served models start at zero replicas, so an idle namespace runs no model.
Reward signals
Nothing in this template writes signals back. A researcher marking a moment as the right one would be the obvious loop and it is not wired.
How reviewer decisions move the thresholds
None. No saves, no flags, no observer marks are recorded by the template as shipped.
None from the application. Mixpeek records retriever executions server-side, which is telemetry about queries rather than feedback about results.
mxp_retriever_executionsThe observers' notes are the ground truth this use case is missing: task start and stop, errors, and confidence per participant. That is an input to evaluation rather than a signal the template collects.
Ask one question across every session and get every participant's answer, attributed. The query that defines this template is the roll-up: one task, every participant, with each result opening at the second it happened.
- Sessions are cut at screen transitions, so a result is a moment with its screen state rather than a timestamp somebody has to scrub to.
- The spoken word and the display are indexed together, which is what lets a claim be read beside the screen it was spoken over.
- The roll-up groups on the participant field, so no participant is silently missing from an answer that summarises a task.
- It runs where you put it, which matters when the footage cannot leave your environment.
For: UX research teams running moderated usability sessions
- sessions
- One object per recording, with study_id, participant_id, rig_id and study_task_id as fields. Those four are what every roll-up groups and filters on, so they are the schema decision that matters.
- session-scenes
- Segment documents with a 1408-dimension multimodal embedding, timing, transcription, and the on-screen text read off that segment.
- session-transcripts
- The spoken side as a 1024-dimension text embedding, carrying the study, participant and task through so a filtered question stays filtered.
One file spins up the namespace. Generated from the workflow above. Also served at /templates/ux-session-analysis.namespace.yaml.
# ux-session-analysis: one manifest spins up the namespace.
# Platform manifest schema (GET /v1/discovery/schema). Validate with POST /v1/manifest/validate,
# apply with POST /v1/manifest/apply or the Deploy button. Wiring comes from the flow diagram:
# edges are bucket -> collection sources, collection -> retriever scope, retriever -> view.
# Applying a SECOND time, over a namespace this template already created: use
# POST /v1/manifest/apply?mode=create_missing, which creates what is missing and leaves
# what exists alone. The default, create_only, fails the WHOLE apply and rolls it back if
# any resource already exists, so an upgrade looks like a dead end without this. Use
# mode=upsert to also patch resources that exist but have drifted from this file.
version: '1.0'
metadata:
name: ux-session-analysis
description: "Namespace template ux-session-analysis. Generated from the flow diagram on mixpeek.com/templates/ux-session-analysis."
namespaces:
- name: ux-session-analysis
description: "Everything below lives in this namespace."
feature_extractors:
- name: multimodal_extractor
version: v1
- name: text_extractor
version: v1
# Daily spend budget, in dollars of what you are charged (1,000 credits a dollar).
# New batches pause once the namespace has spent this much today; queries are not capped.
# Raise it later on the namespace: PATCH /v1/namespaces/<id> {"spend_budget": {"daily_usd": N}}.
budget:
daily_usd: 0.1
# Data sources. A storage connection carries credentials, so it is created in Studio
# (or POST /v1/organizations/storage-connections) and synced into the bucket named here.
# clinic-recordings: s3, continuous, moderated session recordings from your rig, in your account -> bucket sessions
buckets:
- name: sessions
namespace: ux-session-analysis
description: "Meant to be fed by clinic-recordings (s3, continuous). No source is connected yet: applying this manifest creates the bucket only. Connect the source to this bucket in Studio (Syncs) to start the feed."
schema:
properties:
content:
type: video
required: true
study_id:
type: string
required: true
participant_id:
type: string
required: true
study_task_id:
type: string
rig_id:
type: string
# Connect your own storage. Create the two secrets, uncomment, and apply again;
# apply live-tests the connection, so it must have real credentials to succeed.
# Until then the buckets above accept direct uploads.
# storage_connections:
# - name: clinic-recordings-connection
# provider: s3
# description: "Read-only access to the s3 location holding moderated session recordings from your rig, in your account."
# config:
# region: us-east-1
# credentials:
# type: access_key
# access_key_id: ${{ secrets.AWS_ACCESS_KEY_ID }}
# secret_access_key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
# bucket_syncs:
# - name: clinic-recordings-sync
# bucket: sessions
# connection: clinic-recordings-connection
# source_path: "sessions/"
# sync_mode: continuous
# polling_interval_seconds: 300
# skip_duplicates: true
# file_filters:
# include_patterns: ["*.mp4", "*.mov"]
# schema_mapping:
# mappings:
# content:
# target_type: blob
# source: {type: file}
# blob_type: video
collections:
- name: session-scenes
namespace: ux-session-analysis
description: "multimodal_extractor@v1 over bucket sessions. Feeds session-transcripts, find-the-moment, task-roll-up."
source:
type: bucket
bucket: sessions
feature_extractor:
name: multimodal_extractor
version: v1
parameters:
run_transcription: true
run_ocr: true
input_mappings:
video: content
field_passthrough:
- source_path: study_id
- source_path: participant_id
- source_path: rig_id
- source_path: study_task_id
- source_path: file_location
- source_path: segment_id
- source_path: start_time
- source_path: end_time
enabled: true
- name: session-transcripts
namespace: ux-session-analysis
description: "text_extractor@v1 over collection session-scenes. Feeds frustration-monitor, participant-frustration, find-the-moment, task-roll-up."
source:
type: collection
collection: session-scenes
feature_extractor:
name: text_extractor
version: v1
input_mappings:
text: transcription
field_passthrough:
- source_path: study_id
- source_path: participant_id
- source_path: study_task_id
- source_path: segment_id
- source_path: video_segment_url
- source_path: start_time
- source_path: end_time
alert_applications:
- alert: participant-frustration
input_mappings:
- input_key: query
source: {source_type: constant, value: "user expresses frustration or gives up"}
- input_key: document_id
source: {source_type: document_field, path: _internal.document_id}
enabled: true
retrievers:
- name: find-the-moment
namespace: ux-session-analysis
description: "Searches session-scenes, session-transcripts across 2 feature indexes with rrf fusion."
collections:
- session-scenes
- session-transcripts
input_schema:
query:
type: text
required: true
description: "What to look for; searched across every index below"
stages:
- stage_name: search
stage_id: feature_search
parameters:
searches:
- feature_uri: "mixpeek://text_extractor@v1"
query:
input_mode: text
value: "{{INPUT.query}}"
top_k: 25
- feature_uri: "mixpeek://multimodal_extractor@v1/vertex_multimodal_embedding"
query:
input_mode: text
value: "{{INPUT.query}}"
top_k: 25
fusion: rrf
final_top_k: 25
tags:
- template:ux-session-analysis
- name: task-roll-up
namespace: ux-session-analysis
description: "Searches session-transcripts, session-scenes across 1 feature index. Groups documents by participant_id."
collections:
- session-transcripts
- session-scenes
input_schema:
query:
type: text
required: true
description: "What to look for; searched across every index below"
stages:
- stage_name: search
stage_id: feature_search
parameters:
searches:
- feature_uri: "mixpeek://text_extractor@v1"
query:
input_mode: text
value: "{{INPUT.query}}"
top_k: 200
fusion: rrf
final_top_k: 200
- stage_name: group
stage_id: group_by
parameters:
group_by_field: participant_id
tags:
- template:ux-session-analysis
- name: frustration-monitor
namespace: ux-session-analysis
description: "The alert's check: does the transcript just written show a participant frustrated or giving up. Scoped to that one transcript. Materialises into participant-frustration."
collections:
- session-transcripts
input_schema:
query:
type: text
required: true
description: "What to look for; searched across every index below"
document_id:
type: text
required: true
description: "The written document this run is scoped to, supplied by the alert attachment"
stages:
- stage_name: search
stage_id: feature_search
pre_filters:
AND:
- field: _internal.document_id
operator: eq
value: "{{INPUT.document_id}}"
parameters:
searches:
- feature_uri: "mixpeek://text_extractor@v1"
query:
input_mode: text
value: "{{INPUT.query}}"
top_k: 5
min_score: 0.82
fusion: rrf
final_top_k: 5
tags:
- template:ux-session-analysis
alerts:
- name: participant-frustration
namespace: ux-session-analysis
description: "Emails the org notifications address (Settings -> Notifications; unset means opted out) when a new session transcript shows a participant frustrated or giving up on a task."
source: retriever
retriever: frustration-monitor
trigger_on: results
notification_config:
channels:
- channel_type: email
include_matches: true
include_scores: true
enabled: false
# What a run of this template must show (plan 11.14). One realtime step only:
# the sessions bucket requires metadata fields on every object, and a seed
# object carries blobs alone, so no document can be seeded and the document,
# query and alert steps the other templates run have nothing to read here.
acceptance:
max_cost_usd: 0.1
steps:
- name: query-embedding-served
kind: realtime
feature_uri: mixpeek://text_extractor@v1
inputs: {text: "the participant could not find the checkout button"}
# No dims: the step checks the length the index declares where it runs
# (1024 under e5, 384 under the standalone's MiniLM).
expect: {}Read from the manifest, so a part it ships commented out never shows as ready.
What each part needs
- session-scenes
- Deploys and processes documents as they arrive.
- session-transcripts
- Deploys and processes documents as they arrive.
- find-the-moment
- Deploys and answers queries once its collections hold documents.
- task-roll-up
- Deploys and answers queries once its collections hold documents.
- frustration-monitor
- Deploys and answers queries once its collections hold documents.
- group_by
- The one-click manifest includes this stage.
- clinic-recordings
- Create the s3 credentials as manifest secrets, then uncomment the sync. It ships commented out because apply tests a connection as it creates it.
| Component | Role | Source | Today |
|---|---|---|---|
| multimodal_extractor@v1 | Segments each session and produces transcription, on-screen text and a visual embedding per segmentThe @v1 pin is required rather than current: @v2 exposes gemini-embedding-2 and does not carry vertex_multimodal_embedding, so a version bump silently drops the 1408-dimension space. | Built in | Deploys ready |
| text_extractor@v1 | Embeds the spoken transcript so a researcher's typed question reaches the wordsMultilingual E5 large instruct, 1024 dimensions, cosine. | Built in | Deploys ready |
| group_by | The roll-up: one task, every participant, each attributedGrouping on participant_id is what makes 'summarise every user on this task' return every user rather than the loudest few. | Built in | Deploys ready |
The usual choice here. Clinic footage is participant video, and a policy that blocks external upload is the normal starting condition rather than an exception.
Apply the manifest and the namespace comes up with two collections and two retrievers.
- Permissions
- A key with namespace create and collection create. The storage connection is a separate grant and the manifest ships it commented out, because apply live-tests a connection and would roll the whole namespace back on a bad credential.
- Footprint
- Two vector indexes, 1408 and 1024 dimensions, over one segment document per screen transition. At a 5 second median, an hour-long session is on the order of several hundred segments.
None ship with this template. The reference engagement agreed its success criteria in advance and has not scored them, because scoring needs the observers' notes as ground truth.
| Category | Rule |
|---|---|
| Scene segmentation | Cut on screen transitions. The criterion agreed with the reference customer is boundaries within 3 seconds of the moderator's own marks on at least 85% of tasks, which has not been run. |
| Roll-up completeness | One task across N participants returns every participant and none missing. This is checkable without ground truth: compare the group count against the participant count. |
| Time on task | Only meaningful where the rig frames the wheel. Treat it as unavailable on any session whose camera does not, rather than as a low number. |
Unmeasured on this platform path. The criterion the reference customer set is a 60-minute session processed in under 30 minutes with the cost stated, and nobody has run it through to check.
Does this read the participant's face for confusion or frustration?+
No, and it is left out on evidence rather than as a limitation of the platform. On the reference corpus the face is about 53 pixels wide and usable on 1.9% of frames, and where it is large enough the reading tracks head pose: looking down at the display returns anger at high confidence, which is what a participant does for most of a session. Confusion is not a category in off-the-shelf models at all. A wrong reading with a plausible story attached is worse than no reading.
Does it separate the moderator from the participant?+
Not by diarisation. The reference pipeline attributes turns with a phrase heuristic that its own source calls a hint used for counting and never presented as ground truth. If attribution has to be reliable, that is a diarisation model to add rather than something this template already does.
Can it tell me how long a task took?+
Where the camera includes the wheel, yes: hands leaving and returning is the timer, and it agreed with 7 of 9 hand-labelled frames on the sessions where it works. On sessions framed only on the display it cannot, and that is a rig change with lead time rather than a tuning question.
How accurate is the retrieval?+
Unscored, and this page will not invent a number. The criterion agreed with the reference customer is the right moment in the top 10 for at least 80% of researcher queries, scored on sessions nobody tuned against. That needs their query set and their answers.
Can it run where the footage has to stay?+
Yes, and for this use case that is usually the deciding property. The same manifest applies against a customer-hosted deployment, so participant video never leaves the environment you nominate.