Skip to content

Commit 095c465

Browse files
authored
Merge pull request #41 from efdevcon/improve-destino
Improve Destino events
2 parents c2c6127 + 42f4d3b commit 095c465

13 files changed

Lines changed: 461 additions & 255 deletions

File tree

devcon-api/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
{
22
"name": "devcon-api",
3-
"description": "Devcon API exposes information about Devcon, past events, schedules and our video archive. You can find more information at https://github.com/efdevcon/api",
3+
"description": "Devcon API exposes information about Devcon, past events, schedules and our video archive. You can find more information at https://github.com/efdevcon/monorepo/tree/main/devcon-api",
44
"homepage": "https://api.devcon.org",
55
"author": "support@devcon.org",
66
"version": "0.6.2",
77
"license": "MIT",
88
"repository": {
99
"type": "git",
10-
"url": "https://github.com/efdevcon/api"
10+
"url": "https://github.com/efdevcon/monorepo/tree/main/devcon-api"
1111
},
1212
"scripts": {
1313
"build": "tsc --project tsconfig.json && tsc-alias -p tsconfig.json",

devcon-api/src/controllers/destino.ts

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { Request, Response, Router } from 'express'
22
import { destinoApi } from '../services/ai/open-ai/open-ai'
3+
import { apikeyHandler } from '@/middleware/apikey'
34

45
export const destinoRouter = Router()
56

@@ -14,7 +15,10 @@ const generateDestinoEvents = async () => {
1415
destinoRouter.get('/destino', async (req: Request, res: Response) => {
1516
const eventsList = await destinoApi.getAllDestinoEvents()
1617

17-
res.json(eventsList)
18+
// filter out events that don't have a date
19+
const eventsListWithDate = eventsList.filter((event) => event.date)
20+
21+
res.json(eventsListWithDate)
1822
})
1923

2024
destinoRouter.get('/destino/:event', async (req: Request, res: Response) => {
@@ -25,6 +29,52 @@ destinoRouter.get('/destino/:event', async (req: Request, res: Response) => {
2529
res.json(eventData)
2630
})
2731

32+
destinoRouter.get('/regenerate/:eventId', apikeyHandler, async (req: Request, res: Response) => {
33+
// Private route: requires apiKey
34+
// #swagger.ignore = true
35+
const { eventId } = req.params
36+
37+
const eventData = await destinoApi.getDestinoEvent(eventId)
38+
39+
if (!eventData) {
40+
return res.status(404).json({ error: 'Event not found' })
41+
}
42+
43+
try {
44+
console.log('[regenerate-destino-event] Starting event regeneration')
45+
46+
console.log('[regenerate-destino-event] Event data:', eventData)
47+
48+
// Format the event data to match the expected structure
49+
const formattedEvent = {
50+
Id: eventData.event_id,
51+
Name: eventData.name,
52+
Location: eventData.location,
53+
Date: { startDate: eventData.date },
54+
Type: eventData.type_of_event,
55+
Twitter: eventData.twitter_handle,
56+
Link: eventData.link,
57+
TargetAudience: eventData.target_audience,
58+
Details: eventData.details,
59+
LastModifiedDate: eventData.last_modified_at,
60+
}
61+
62+
const updatedEventData = await destinoApi.generateDestinoEvent(formattedEvent, true)
63+
64+
console.log('[regenerate-destino-event] Generated event result: OK', updatedEventData)
65+
66+
if (!updatedEventData) {
67+
console.error('[regenerate-destino-event] No event data returned from generateDestinoEvent')
68+
return res.status(500).json({ error: 'Failed to generate event' })
69+
}
70+
71+
res.json(updatedEventData)
72+
} catch (error: any) {
73+
console.error('[regenerate-destino-event] Error generating event:', error)
74+
res.status(500).json({ error: 'Internal server error', details: error.message })
75+
}
76+
})
77+
2878
generateDestinoEvents()
2979

3080
// Refresh events every hour
272 KB
Loading

devcon-api/src/services/ai/open-ai/open-ai.ts

Lines changed: 152 additions & 99 deletions
Original file line numberDiff line numberDiff line change
@@ -517,6 +517,15 @@ export const destinoApi = (() => {
517517
}
518518
updated_at: string
519519
last_modified_at: string
520+
name: string
521+
location: string
522+
date: string
523+
type_of_event: string
524+
twitter_handle: string
525+
link: string
526+
target_audience: string
527+
details: string
528+
image_url?: string
520529
}
521530

522531
// Initialize Supabase client once to reuse across functions
@@ -535,128 +544,172 @@ export const destinoApi = (() => {
535544

536545
return data as EventRecord | null
537546
},
538-
generateDestinoEvent: async (event: any) => {
539-
const eventRecord = await _interface.getDestinoEvent(event.Id)
540-
const eventExists = !!eventRecord
541-
542-
// If exists and updated after last modification, return cached content
543-
if (eventRecord) {
544-
// console.log('record found')
545-
// console.log('updated_at:', eventRecord.updated_at)
546-
// console.log('LastModifiedDate:', event.LastModifiedDate)
547-
// console.log('updated_at date:', new Date(eventRecord.updated_at))
548-
// console.log('LastModifiedDate date:', new Date(event.LastModifiedDate))
549-
// console.log('comparison result:', new Date(eventRecord.updated_at) > new Date(event.LastModifiedDate))
550-
551-
if (new Date(eventRecord.updated_at) > new Date(event.LastModifiedDate)) {
547+
generateDestinoEvent: async (event: any, forceImageGeneration = false) => {
548+
try {
549+
console.log(`[generateDestinoEvent] Starting for event ${event.Id}${forceImageGeneration ? ' (force image generation)' : ''}`)
550+
551+
if (!event || !event.Id) {
552+
console.error('[generateDestinoEvent] Invalid event data:', event)
553+
throw new Error('Invalid event data')
554+
}
555+
556+
const eventRecord = await _interface.getDestinoEvent(event.Id)
557+
const eventExists = !!eventRecord
558+
console.log(`[generateDestinoEvent] Event exists in database: ${eventExists}`)
559+
560+
// If exists and updated after last modification, return cached content
561+
if (!forceImageGeneration && eventRecord && new Date(eventRecord.updated_at) > new Date(event.LastModifiedDate)) {
552562
return eventRecord.content
553563
}
554-
}
555564

556-
// Otherwise generate new content
557-
console.log(`Generating new content for Destino event ${event.Id}`)
558-
559-
const upsert = {
560-
event_id: event.Id,
561-
twitter_handle: event.Twitter,
562-
type_of_event: event['Type of Event'],
563-
location: event.Location,
564-
link: event.Link,
565-
name: event.Name,
566-
date: event.Date.startDate,
567-
target_audience: event.TargetAudience,
568-
details: event.Details,
569-
updated_at: new Date().toISOString(),
570-
last_modified_at: event.LastModifiedDate,
571-
} as any
572-
573-
if (!eventExists) {
574-
const eventCompletion = await openai.beta.chat.completions.parse({
575-
temperature: 0,
576-
model: 'gpt-4.1',
577-
messages: [
578-
{
579-
role: 'system',
580-
content:
581-
'You take an event object and generate a simple summary of it in English, Spanish and Portuguese. Max 500 characters. It will be used to advertise the event.',
582-
},
583-
{ role: 'user', content: JSON.stringify({ ...event, description: '' }) },
584-
],
585-
response_format: zodResponseFormat(EventSchema, 'summary'),
586-
})
565+
// Otherwise generate new content
566+
console.log(`[generateDestinoEvent] Generating new content for event ${event.Id}`)
567+
568+
const upsert = {
569+
event_id: event.Id,
570+
twitter_handle: event.Twitter,
571+
type_of_event: event['Type of Event'],
572+
location: event.Location,
573+
link: event.Link,
574+
name: event.Name,
575+
date: event.Date.startDate,
576+
target_audience: event.TargetAudience,
577+
details: event.Details,
578+
updated_at: new Date().toISOString(),
579+
last_modified_at: event.LastModifiedDate,
580+
} as any
581+
582+
if (!eventExists || forceImageGeneration) {
583+
console.log(`[generateDestinoEvent] Generating multilingual content for event ${event.Id}`)
584+
const eventCompletion = await openai.beta.chat.completions.parse({
585+
temperature: 0,
586+
model: 'gpt-4.1',
587+
messages: [
588+
{
589+
role: 'system',
590+
content:
591+
'You take an event object and generate a simple summary of it in English, Spanish and Portuguese. Max 500 characters. It will be used to advertise the event.',
592+
},
593+
{ role: 'user', content: JSON.stringify({ ...event, description: '' }) },
594+
],
595+
response_format: zodResponseFormat(EventSchema, 'summary'),
596+
})
587597

588-
const content = eventCompletion.choices[0].message.parsed as { en: string; es: string; pt: string }
598+
if (!eventCompletion.choices?.[0]?.message?.parsed) {
599+
console.error('[generateDestinoEvent] Failed to generate content:', eventCompletion)
600+
throw new Error('Failed to generate content')
601+
}
589602

590-
const prompt = `
591-
Adjust the reference image to match the event. Don't use any text in the generated image.
592-
593-
Event name: ${event.Name}
594-
Event location: ${event.Location}
595-
Event summary: ${content.en}
596-
`
603+
// Fetch reference image from Supabase Storage
604+
const { data: imageData, error: imageError } = await supabase.storage.from('destino-events').download('reference/destino.png')
597605

598-
// Fetch reference image from Supabase Storage
599-
const { data: imageData, error: imageError } = await supabase.storage.from('destino-events').download('reference/destino.png')
606+
if (imageError) {
607+
console.error('Error fetching reference image:', imageError)
608+
throw new Error('Failed to fetch reference image')
609+
}
600610

601-
if (imageError) {
602-
console.error('Error fetching reference image:', imageError)
603-
throw new Error('Failed to fetch reference image')
604-
}
611+
const openAICompatibleImage = await toFile(imageData, null, { type: 'image/png' })
605612

606-
const openAICompatibleImage = await toFile(imageData, null, { type: 'image/png' })
613+
const content = eventCompletion.choices[0].message.parsed as { en: string; es: string; pt: string }
614+
console.log(`[generateDestinoEvent] Generated content for event ${event.Id}:`, {
615+
en: content.en.substring(0, 50) + '...',
616+
es: content.es.substring(0, 50) + '...',
617+
pt: content.pt.substring(0, 50) + '...',
618+
})
607619

608-
const resultImage = await openai.images.edit({
609-
model: 'gpt-image-1',
610-
prompt,
611-
image: openAICompatibleImage,
612-
// @ts-ignore
613-
size: '1536x1024',
614-
n: 1,
615-
})
620+
const prompt = `Adjust the reference image to suit the context of the event.
621+
DO NOT add or include any text in the generated image.
622+
Keep composition of the reference image.
623+
624+
Context:
625+
- Event name: "${event.Name}"
626+
- Event location: "${event.Location}"
627+
DO NOT include Event name or event location in the generated image.`
628+
console.log(`[generateDestinoEvent] Generating image for event ${event.Id} with prompt:`, prompt)
629+
630+
const resultImage = await openai.images.edit({
631+
model: 'gpt-image-1',
632+
prompt,
633+
image: openAICompatibleImage,
634+
// @ts-ignore
635+
size: '1536x1024',
636+
n: 1,
637+
})
616638

617-
// Save the image to a file
618-
const image_base64 = resultImage.data?.[0]?.b64_json
619-
const image_bytes = image_base64 ? Buffer.from(image_base64, 'base64') : null
639+
if (!resultImage.data?.[0]?.b64_json) {
640+
console.error('[generateDestinoEvent] Failed to generate image:', resultImage)
641+
throw new Error('Failed to generate image')
642+
}
620643

621-
let imageUrl = null
644+
// Save the image to a file
645+
const image_base64 = resultImage.data[0].b64_json
646+
const image_bytes = Buffer.from(image_base64, 'base64')
647+
console.log(`[generateDestinoEvent] Image generated for event ${event.Id}: ${image_bytes ? 'success' : 'failed'}`)
622648

623-
if (image_bytes) {
624-
// Twitter resize
625-
const resizedImage = await sharp(image_bytes).resize(1200, 675, { fit: 'cover' }).toBuffer()
649+
let imageUrl = null
626650

627-
// Upload to Supabase Storage
628-
const { data: uploadData1, error: uploadError1 } = await supabase.storage
629-
.from('destino-events')
630-
.upload(`${event.Id}-twitter.png`, resizedImage, {
651+
if (image_bytes) {
652+
console.log(`[generateDestinoEvent] Processing and uploading images for event ${event.Id}`)
653+
// Social media resize
654+
const resizedImage = await sharp(image_bytes).resize(1200, 628, { fit: 'cover' }).jpeg({ quality: 90 }).toBuffer()
655+
656+
// Generate timestamp for versioning
657+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-')
658+
const socialImagePath = `${event.Id}-social-${timestamp}.jpg`
659+
const originalImagePath = `${event.Id}-original-${timestamp}.png`
660+
661+
// Upload to Supabase Storage
662+
const { data: uploadData1, error: uploadError1 } = await supabase.storage.from('destino-events').upload(socialImagePath, resizedImage, {
663+
contentType: 'image/jpeg',
664+
upsert: true,
665+
})
666+
667+
const { data: uploadData2, error: uploadError2 } = await supabase.storage.from('destino-events').upload(originalImagePath, image_bytes, {
631668
contentType: 'image/png',
632669
upsert: true,
633670
})
634671

635-
const { data: uploadData2, error: uploadError2 } = await supabase.storage.from('destino-events').upload(`${event.Id}.png`, image_bytes, {
636-
contentType: 'image/png',
637-
upsert: true,
638-
})
672+
if (uploadError1 || uploadError2) {
673+
console.error(`[generateDestinoEvent] Error uploading images for event ${event.Id}:`, uploadError1 || uploadError2)
674+
throw new Error('Failed to upload images')
675+
} else {
676+
// Get public URL
677+
const {
678+
data: { publicUrl },
679+
} = supabase.storage.from('destino-events').getPublicUrl(socialImagePath)
680+
681+
imageUrl = publicUrl
682+
console.log(`[generateDestinoEvent] Images uploaded successfully for event ${event.Id}. URL: ${imageUrl}`)
683+
}
684+
}
685+
686+
upsert.image_url = imageUrl
687+
upsert.content = content
688+
}
689+
690+
// Save to Supabase
691+
console.log(`[generateDestinoEvent] Saving event ${event.Id} to database`)
692+
const result = await supabase.from('destino_events').upsert(upsert, { defaultToNull: false })
693+
console.log(`[generateDestinoEvent] Event ${event.Id} saved successfully`)
639694

640-
if (uploadError1 || uploadError2) {
641-
console.error('Error uploading image:', uploadError1 || uploadError2)
642-
} else {
643-
// Get public URL
644-
const {
645-
data: { publicUrl },
646-
} = supabase.storage.from('destino-events').getPublicUrl(`${event.Id}.png`)
695+
// If forceImageGeneration is true, return the image URL
696+
if (forceImageGeneration) {
697+
// update image_url in the database
698+
await supabase.from('destino_events').update({ image_url: upsert.image_url }).eq('event_id', event.Id)
647699

648-
imageUrl = publicUrl
700+
console.log(`[generateDestinoEvent] Returning content and image URL for event ${event.Id}`)
701+
return {
702+
content: upsert.content,
703+
imageUrl: upsert.image_url,
704+
updated: true,
649705
}
650706
}
651707

652-
upsert.image_url = imageUrl
653-
upsert.content = content
708+
return result
709+
} catch (error: any) {
710+
console.error('[generateDestinoEvent] Error:', error)
711+
throw error
654712
}
655-
656-
// Save to Supabase
657-
const result = await supabase.from('destino_events').upsert(upsert, { defaultToNull: false })
658-
659-
return result
660713
},
661714
generateDestinoEvents: async () => {
662715
const events = await fetchFromSalesforce()

0 commit comments

Comments
 (0)