Аналіз емоцій AI під час відеозвонків у мобільних додатках
Аналіз емоцій через камеру в реальному часі технічно можливий, але вимагає особливої уваги до етики та UX. Ось технічна сторона без прихування обмежень: моделі аналізу емоцій — одні з найкритикованіших інструментів AI за надійність.
Важливе обмеження, яке не слід ігнорувати
Академічний консенсус (Lisa Feldman Barrett, 2019) та практика показують: вираз обличчя не однозначно відображає емоції. Один і той самий паттерн руху лицьових м'язів означає різне для різних людей та культур. Тому:
- Називати результат «емоцією» некоректно—«афективний стан» або «вираз обличчя» точніше
- Системи ніколи не повинні використовуватися для кадрових чи юридичних рішень
- Користувачі повинні явно дати згоду на аналіз свого обличчя
Це не просто етична примітка—це архітектурна вимога.
Технічний стек
Детекція обличчя — MediaPipe Face Detection (iOS/Android), Vision VNDetectFaceRectanglesRequest (iOS).
Розпізнавання виразів — кілька варіантів:
- Apple Vision
VNDetectFaceExpressionsRequest(iOS 17+) — вбудований, без хмари, 7 базових Action Units - Microsoft Azure Face API — хмарний, детальний, включає Action Units
- AWS Rekognition (DetectFaces) — хмарний, 7 базових емоцій
- FER+ модель (TFLite/CoreML) — open source, 8 класів, on-device
Для відеозвонків on-device обов'язково: не можна стримити обличчя людини в хмару без явної згоди.
Реалізація на iOS з Vision (On-Device)
// iOS 17+: аналіз виразу обличчя через Vision
class FaceExpressionAnalyzer {
func analyze(sampleBuffer: CMSampleBuffer) async throws -> ExpressionResult? {
guard let pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else { return nil }
let faceRequest = VNDetectFaceLandmarksRequest()
// iOS 17: аналіз виразів — brow action units, тощо
let expressionRequest = VNDetectFaceExpressionsRequest()
let handler = VNImageRequestHandler(cvPixelBuffer: pixelBuffer)
try handler.perform([faceRequest, expressionRequest])
guard let faceObs = faceRequest.results?.first as? VNFaceObservation,
let exprObs = expressionRequest.results?.first as? VNFaceExpressionObservation else {
return nil
}
return ExpressionResult(
faceBox: faceObs.boundingBox,
browLower: exprObs.browLowerQuirk,
browRaise: exprObs.browRaiseRight + exprObs.browRaiseLeft,
eyesClosed: exprObs.eyeBlinkLeft + exprObs.eyeBlinkRight,
mouthSmile: exprObs.mouthSmileLeft + exprObs.mouthSmileRight,
mouthFrown: exprObs.mouthFrownLeft + exprObs.mouthFrownRight,
mouthOpen: exprObs.mouthOpen,
jawOpen: exprObs.jawOpen
)
}
}
VNDetectFaceExpressionsRequest працює з Action Units—базовими рухами лицьових м'язів з FACS (Facial Action Coding System). Це коректніше, ніж «посмішка = щастя»: конкретна дія м'яза, без інтерпретації.
Агрегація за часом
Один кадр — шум. Використовуйте агрегацію по ковзному вікну:
class ExpressionAggregator {
private var history: [ExpressionResult] = []
private let windowSize = 15 // ~0.5 сек при 30fps
func update(_ result: ExpressionResult) -> AggregatedExpression {
history.append(result)
if history.count > windowSize { history.removeFirst() }
return AggregatedExpression(
averageSmile: history.map { $0.mouthSmile }.average(),
averageBrowRaise: history.map { $0.browRaise }.average(),
averageJawOpen: history.map { $0.jawOpen }.average(),
// Тренд: посмішка зростає або падає за останні N кадрів
smileTrend: computeTrend(history.map { $0.mouthSmile })
)
}
}
Інтеграція у відеозвонки
Аналіз виконується на локальному відеопотоці з вашої камери, не з потоку опонента. Потік опонента на їхньому пристрої; у вас немає доступу до сирих кадрів через стандартний WebRTC. Два підходи:
SDK з підтримкою аналізу — Agora Video SDK дозволяє локальний відеопроцесор:
// Agora: обробка локального відео перед відправкою
class EmotionVideoProcessor: AgoraVideoFrameDelegate {
func onCapture(_ videoFrame: AgoraOutputVideoFrame,
sourceType: AgoraVideoSourceType) -> Bool {
// Аналізуйте своє обличчя перед відправкою
if let pixelBuffer = videoFrame.pixelBuffer {
Task {
let result = try? await expressionAnalyzer.analyze(buffer: pixelBuffer)
// результат аналізує ваші емоції, не опонента
await MainActor.run {
emotionDelegate?.didUpdateExpression(result)
}
}
}
return true // передайте кадр у потік без змін
}
}
Peer-to-peer аналіз — обидва учасники аналізують свої власні вирази та передають результати (не відео) через data channel. WebRTC data channel для JSON пакетів—мінімальні витрати.
// Передача даних про емоції через WebRTC DataChannel
struct EmotionDataPacket: Codable {
let timestamp: Double
let smile: Float
let browRaise: Float
let eyesClosed: Float
// НЕ передавайте зображення—лише числа
}
func sendEmotionData(_ expression: AggregatedExpression) {
let packet = EmotionDataPacket(
timestamp: Date().timeIntervalSince1970,
smile: expression.averageSmile,
browRaise: expression.averageBrowRaise,
eyesClosed: expression.averageJawOpen
)
let data = try! JSONEncoder().encode(packet)
dataChannel.sendData(RTCDataBuffer(data: data, isBinary: false))
}
Кожен учасник аналізує лише себе, але видить агреговані дані від опонента. Приватно та технічно чисто.
UX: як показувати результати
Показування «злий / сумний / щасливий» некоректно та потенційно образливо. Правильні варіанти:
- Індикатор залучення: «опонент активно бере участь» (на основі browRaise + eyeBlink ритму)
- Рівень уваги: нейтральний індикатор залучення без інтерпретації емоцій
- Настрій розмови: агрегація обох учасників в єдиний «тепловий» показник
@Composable
fun EngagementIndicator(score: Float) {
Box(
modifier = Modifier
.size(12.dp)
.clip(CircleShape)
.background(
when {
score > 0.7f -> Color(0xFF4CAF50) // залучено
score > 0.4f -> Color(0xFFFFC107) // нейтрально
else -> Color(0xFF9E9E9E) // пасивно
}
)
)
}
Жодних обличь з емоціями, жодних словесних міток—лише нейтральний кольоровий індикатор.
Кошторис за часом
On-device аналіз виразу через Vision + базовий індикатор залучення у існуючому відеозвонку займає 1–2 тижні. Повна система з peer-to-peer передачею даних через data channel, агрегацією, аналітикою розмови, екраном згоди та підтримкою iOS + Android вимагає 2–4 тижнів.







