Include voice recording payload in analysis
Build and deploy Worker / build (push) Successful in 53s

This commit is contained in:
Ruslan Bakiev
2026-06-12 21:09:07 +07:00
parent 321eab6d6d
commit 5021b55537
8 changed files with 2599 additions and 167 deletions
+33 -10
View File
@@ -17,16 +17,25 @@ enum VoiceExperienceStatus {
} }
model Place { model Place {
id String @id @default(cuid()) id String @id @default(cuid())
googlePlaceId String @unique googlePlaceId String @unique
name String name String
latitude Float latitude Float
longitude Float longitude Float
googlePrimaryType String? googlePrimaryType String?
googleTypes String[] @default([]) googleTypes String[] @default([])
experiences VoiceExperience[] googleBusinessStatus String?
createdAt DateTime @default(now()) googleRating Float?
updatedAt DateTime @updatedAt googleUserRatingCount Int?
googleRegularOpeningHours Json?
googleCurrentOpeningHours Json?
googlePhotos Json?
googlePayload Json?
googlePayloadFetchedAt DateTime?
experiences VoiceExperience[]
favoriteUsers UserFavoritePlace[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
} }
model User { model User {
@@ -39,10 +48,22 @@ model User {
languageCode String? languageCode String?
isAdmin Boolean @default(false) isAdmin Boolean @default(false)
voiceExperiences VoiceExperience[] voiceExperiences VoiceExperience[]
favoritePlaces UserFavoritePlace[]
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
} }
model UserFavoritePlace {
id String @id @default(cuid())
userId String
user User @relation(fields: [userId], references: [id])
placeId String
place Place @relation(fields: [placeId], references: [id])
createdAt DateTime @default(now())
@@unique([userId, placeId])
}
model VoiceExperience { model VoiceExperience {
id String @id @default(cuid()) id String @id @default(cuid())
placeId String placeId String
@@ -57,6 +78,8 @@ model VoiceExperience {
status VoiceExperienceStatus @default(UPLOADED) status VoiceExperienceStatus @default(UPLOADED)
transcript String? transcript String?
analysis Json? analysis Json?
recordingPayload Json?
promptHintsShown String[] @default([])
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
} }
File diff suppressed because one or more lines are too long
+23 -5
View File
@@ -128,6 +128,14 @@ exports.Prisma.PlaceScalarFieldEnum = {
longitude: 'longitude', longitude: 'longitude',
googlePrimaryType: 'googlePrimaryType', googlePrimaryType: 'googlePrimaryType',
googleTypes: 'googleTypes', googleTypes: 'googleTypes',
googleBusinessStatus: 'googleBusinessStatus',
googleRating: 'googleRating',
googleUserRatingCount: 'googleUserRatingCount',
googleRegularOpeningHours: 'googleRegularOpeningHours',
googleCurrentOpeningHours: 'googleCurrentOpeningHours',
googlePhotos: 'googlePhotos',
googlePayload: 'googlePayload',
googlePayloadFetchedAt: 'googlePayloadFetchedAt',
createdAt: 'createdAt', createdAt: 'createdAt',
updatedAt: 'updatedAt' updatedAt: 'updatedAt'
}; };
@@ -145,6 +153,13 @@ exports.Prisma.UserScalarFieldEnum = {
updatedAt: 'updatedAt' updatedAt: 'updatedAt'
}; };
exports.Prisma.UserFavoritePlaceScalarFieldEnum = {
id: 'id',
userId: 'userId',
placeId: 'placeId',
createdAt: 'createdAt'
};
exports.Prisma.VoiceExperienceScalarFieldEnum = { exports.Prisma.VoiceExperienceScalarFieldEnum = {
id: 'id', id: 'id',
placeId: 'placeId', placeId: 'placeId',
@@ -157,6 +172,8 @@ exports.Prisma.VoiceExperienceScalarFieldEnum = {
status: 'status', status: 'status',
transcript: 'transcript', transcript: 'transcript',
analysis: 'analysis', analysis: 'analysis',
recordingPayload: 'recordingPayload',
promptHintsShown: 'promptHintsShown',
createdAt: 'createdAt', createdAt: 'createdAt',
updatedAt: 'updatedAt' updatedAt: 'updatedAt'
}; };
@@ -176,16 +193,16 @@ exports.Prisma.QueryMode = {
insensitive: 'insensitive' insensitive: 'insensitive'
}; };
exports.Prisma.NullsOrder = {
first: 'first',
last: 'last'
};
exports.Prisma.JsonNullValueFilter = { exports.Prisma.JsonNullValueFilter = {
DbNull: Prisma.DbNull, DbNull: Prisma.DbNull,
JsonNull: Prisma.JsonNull, JsonNull: Prisma.JsonNull,
AnyNull: Prisma.AnyNull AnyNull: Prisma.AnyNull
}; };
exports.Prisma.NullsOrder = {
first: 'first',
last: 'last'
};
exports.VoiceExperienceStatus = exports.$Enums.VoiceExperienceStatus = { exports.VoiceExperienceStatus = exports.$Enums.VoiceExperienceStatus = {
UPLOADED: 'UPLOADED', UPLOADED: 'UPLOADED',
TRANSCRIBING: 'TRANSCRIBING', TRANSCRIBING: 'TRANSCRIBING',
@@ -198,6 +215,7 @@ exports.VoiceExperienceStatus = exports.$Enums.VoiceExperienceStatus = {
exports.Prisma.ModelName = { exports.Prisma.ModelName = {
Place: 'Place', Place: 'Place',
User: 'User', User: 'User',
UserFavoritePlace: 'UserFavoritePlace',
VoiceExperience: 'VoiceExperience' VoiceExperience: 'VoiceExperience'
}; };
+2433 -118
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -1,5 +1,5 @@
{ {
"name": "prisma-client-404088dc715856cb4ed2e7f2d4b8eb35c87427ce32c0e67bc04d6ff67961fc2b", "name": "prisma-client-baa0475e783dd87e65df5f828f4a43807c3762e0c66b9d15b841a12e00ad0bd4",
"main": "index.js", "main": "index.js",
"types": "index.d.ts", "types": "index.d.ts",
"browser": "default.js", "browser": "default.js",
+38 -15
View File
@@ -17,30 +17,51 @@ enum VoiceExperienceStatus {
} }
model Place { model Place {
id String @id @default(cuid()) id String @id @default(cuid())
googlePlaceId String @unique googlePlaceId String @unique
name String name String
latitude Float latitude Float
longitude Float longitude Float
googlePrimaryType String? googlePrimaryType String?
googleTypes String[] @default([]) googleTypes String[] @default([])
experiences VoiceExperience[] googleBusinessStatus String?
createdAt DateTime @default(now()) googleRating Float?
updatedAt DateTime @updatedAt googleUserRatingCount Int?
googleRegularOpeningHours Json?
googleCurrentOpeningHours Json?
googlePhotos Json?
googlePayload Json?
googlePayloadFetchedAt DateTime?
experiences VoiceExperience[]
favoriteUsers UserFavoritePlace[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
} }
model User { model User {
id String @id @default(cuid()) id String @id @default(cuid())
telegramId String @unique telegramId String @unique
username String? username String?
firstName String? firstName String?
lastName String? lastName String?
photoUrl String? photoUrl String?
languageCode String? languageCode String?
isAdmin Boolean @default(false) isAdmin Boolean @default(false)
voiceExperiences VoiceExperience[] voiceExperiences VoiceExperience[]
createdAt DateTime @default(now()) favoritePlaces UserFavoritePlace[]
updatedAt DateTime @updatedAt createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model UserFavoritePlace {
id String @id @default(cuid())
userId String
user User @relation(fields: [userId], references: [id])
placeId String
place Place @relation(fields: [placeId], references: [id])
createdAt DateTime @default(now())
@@unique([userId, placeId])
} }
model VoiceExperience { model VoiceExperience {
@@ -57,6 +78,8 @@ model VoiceExperience {
status VoiceExperienceStatus @default(UPLOADED) status VoiceExperienceStatus @default(UPLOADED)
transcript String? transcript String?
analysis Json? analysis Json?
recordingPayload Json?
promptHintsShown String[] @default([])
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
} }
+17
View File
@@ -26,6 +26,11 @@ type PlaceAnalysis = {
summary: string; summary: string;
}; };
type RecordingContext = {
promptHintsShown: string[];
recordingPayload: unknown;
};
export async function analyzeVoiceExperience(experienceId: string) { export async function analyzeVoiceExperience(experienceId: string) {
const experience = await prisma.voiceExperience.findUnique({ const experience = await prisma.voiceExperience.findUnique({
where: { id: experienceId }, where: { id: experienceId },
@@ -48,6 +53,8 @@ export async function analyzeVoiceExperience(experienceId: string) {
const analysis = await buildPlaceAnalysis({ const analysis = await buildPlaceAnalysis({
placeName: experience.place.name, placeName: experience.place.name,
transcript: experience.transcript, transcript: experience.transcript,
promptHintsShown: experience.promptHintsShown,
recordingPayload: experience.recordingPayload,
}); });
await prisma.voiceExperience.update({ await prisma.voiceExperience.update({
@@ -64,6 +71,8 @@ export async function analyzeVoiceExperience(experienceId: string) {
async function buildPlaceAnalysis(input: { async function buildPlaceAnalysis(input: {
placeName: string; placeName: string;
transcript: string; transcript: string;
promptHintsShown: string[];
recordingPayload: unknown;
}): Promise<PlaceAnalysis> { }): Promise<PlaceAnalysis> {
if (!config.openRouterApiKey) { if (!config.openRouterApiKey) {
throw new Error('OPENROUTER_API_KEY is required for voice analysis.'); throw new Error('OPENROUTER_API_KEY is required for voice analysis.');
@@ -146,6 +155,7 @@ async function buildPlaceAnalysis(input: {
}, },
placeName: input.placeName, placeName: input.placeName,
ontology, ontology,
recordingContext: recordingContext(input),
transcript: input.transcript, transcript: input.transcript,
}), }),
}, },
@@ -174,6 +184,13 @@ async function buildPlaceAnalysis(input: {
return analysis; return analysis;
} }
function recordingContext(input: RecordingContext) {
return {
promptHintsShown: input.promptHintsShown,
recordingPayload: input.recordingPayload,
};
}
function assertValidAnalysis(analysis: PlaceAnalysis) { function assertValidAnalysis(analysis: PlaceAnalysis) {
const allowedTags = new Set( const allowedTags = new Set(
placeOntology.flatMap((axis) => placeOntology.flatMap((axis) =>