@@ -39,6 +39,27 @@ interface SongGenerationRequest {
3939 tags ?: string ;
4040}
4141
42+ /**
43+ * Interface for stored generated songs
44+ */
45+ interface GeneratedSong {
46+ id : string ;
47+ clipId : string ;
48+ title : string ;
49+ status : string ;
50+ audioUrl : string | null ;
51+ imageUrl : string | null ;
52+ createdAt : Date ;
53+ completedAt : Date | null ;
54+ userId : string ;
55+ prompt : string ;
56+ tags : string ;
57+ selectedPhotosCount : number ;
58+ selectedTranscriptionsCount : number ;
59+ metadata : any ;
60+ favorite ?: boolean ;
61+ }
62+
4263const PACKAGE_NAME = process . env . PACKAGE_NAME ?? ( ( ) => { throw new Error ( 'PACKAGE_NAME is not set in .env file' ) ; } ) ( ) ;
4364const MENTRAOS_API_KEY = process . env . MENTRAOS_API_KEY ?? ( ( ) => { throw new Error ( 'MENTRAOS_API_KEY is not set in .env file' ) ; } ) ( ) ;
4465const PORT = parseInt ( process . env . PORT || '3000' ) ;
@@ -66,6 +87,10 @@ class ExampleMentraOSApp extends AppServer {
6687 private photoGalleries : Map < string , StoredPhoto [ ] > = new Map ( ) ;
6788 // Transcription history: userId -> array of transcriptions
6889 private transcriptionHistory : Map < string , TranscriptionEntry [ ] > = new Map ( ) ;
90+ // Generated songs gallery: userId -> array of songs
91+ private songGalleries : Map < string , GeneratedSong [ ] > = new Map ( ) ;
92+ // Active song generations: clipId -> userId (for tracking ongoing generations)
93+ private activeGenerations : Map < string , string > = new Map ( ) ;
6994 // Streaming state
7095 private isStreamingPhotos : Map < string , boolean > = new Map ( ) ;
7196 private nextPhotoTime : Map < string , number > = new Map ( ) ;
@@ -244,6 +269,81 @@ class ExampleMentraOSApp extends AppServer {
244269 return gallery && gallery . length > 0 ? gallery [ gallery . length - 1 ] : undefined ;
245270 }
246271
272+ /**
273+ * Add a generated song to the user's gallery
274+ */
275+ private addSongToGallery ( userId : string , clipId : string , prompt : string , tags : string , selectedPhotosCount : number , selectedTranscriptionsCount : number ) : GeneratedSong {
276+ if ( ! this . songGalleries . has ( userId ) ) {
277+ this . songGalleries . set ( userId , [ ] ) ;
278+ }
279+
280+ const song : GeneratedSong = {
281+ id : `song-${ Date . now ( ) } -${ Math . random ( ) . toString ( 36 ) . substr ( 2 , 9 ) } ` ,
282+ clipId,
283+ title : '' , // Will be updated when available
284+ status : 'submitted' ,
285+ audioUrl : null ,
286+ imageUrl : null ,
287+ createdAt : new Date ( ) ,
288+ completedAt : null ,
289+ userId,
290+ prompt,
291+ tags,
292+ selectedPhotosCount,
293+ selectedTranscriptionsCount,
294+ metadata : { } ,
295+ favorite : false
296+ } ;
297+
298+ const gallery = this . songGalleries . get ( userId ) ! ;
299+ gallery . push ( song ) ;
300+
301+ // Keep only the last 25 songs per user
302+ if ( gallery . length > 25 ) {
303+ gallery . shift ( ) ;
304+ }
305+
306+ // Track active generation
307+ this . activeGenerations . set ( clipId , userId ) ;
308+
309+ this . logger . info ( `Added song to gallery for user ${ userId } , clipId: ${ clipId } ` ) ;
310+ return song ;
311+ }
312+
313+ /**
314+ * Update song status and metadata
315+ */
316+ private updateSongInGallery ( clipId : string , status : string , title ?: string , audioUrl ?: string , imageUrl ?: string , metadata ?: any ) : void {
317+ const userId = this . activeGenerations . get ( clipId ) ;
318+ if ( ! userId ) return ;
319+
320+ const gallery = this . songGalleries . get ( userId ) ;
321+ if ( ! gallery ) return ;
322+
323+ const song = gallery . find ( s => s . clipId === clipId ) ;
324+ if ( ! song ) return ;
325+
326+ song . status = status ;
327+ if ( title ) song . title = title ;
328+ if ( audioUrl ) song . audioUrl = audioUrl ;
329+ if ( imageUrl ) song . imageUrl = imageUrl ;
330+ if ( metadata ) song . metadata = metadata ;
331+
332+ if ( status === 'complete' ) {
333+ song . completedAt = new Date ( ) ;
334+ this . activeGenerations . delete ( clipId ) ; // Clean up tracking
335+ }
336+
337+ this . logger . debug ( `Updated song ${ song . id } for user ${ userId } , status: ${ status } ` ) ;
338+ }
339+
340+ /**
341+ * Get user's song gallery
342+ */
343+ private getSongGallery ( userId : string ) : GeneratedSong [ ] {
344+ return this . songGalleries . get ( userId ) || [ ] ;
345+ }
346+
247347
248348
249349 /**
@@ -460,9 +560,20 @@ class ExampleMentraOSApp extends AppServer {
460560 const result = await response . json ( ) ;
461561 this . logger . info ( `Song generation started for user ${ userId } , clip ID: ${ result . id } ` ) ;
462562
563+ // Save to song gallery
564+ const song = this . addSongToGallery (
565+ userId ,
566+ result . id ,
567+ songPrompt . slice ( 0 , 2500 ) ,
568+ tags || 'ambient, atmospheric, reflective' ,
569+ selectedPhotos . length ,
570+ selectedTranscriptions . length
571+ ) ;
572+
463573 res . json ( {
464574 success : true ,
465575 clipId : result . id ,
576+ songId : song . id ,
466577 status : result . status ,
467578 prompt : songPrompt . slice ( 0 , 2500 ) ,
468579 selectedPhotos : selectedPhotos . length ,
@@ -510,6 +621,16 @@ class ExampleMentraOSApp extends AppServer {
510621 const clips = await response . json ( ) ;
511622 const clip = clips [ 0 ] ;
512623
624+ // Update song in gallery
625+ this . updateSongInGallery (
626+ clipId ,
627+ clip . status ,
628+ clip . title ,
629+ clip . audio_url ,
630+ clip . image_url ,
631+ clip . metadata
632+ ) ;
633+
513634 res . json ( {
514635 id : clip . id ,
515636 status : clip . status ,
@@ -526,6 +647,90 @@ class ExampleMentraOSApp extends AppServer {
526647 }
527648 } ) ;
528649
650+ // API endpoint to get user's song gallery
651+ app . get ( '/api/songs' , ( req : any , res : any ) => {
652+ const userId = ( req as AuthenticatedRequest ) . authUserId ;
653+
654+ if ( ! userId ) {
655+ res . status ( 401 ) . json ( { error : 'Not authenticated' } ) ;
656+ return ;
657+ }
658+
659+ const songs = this . getSongGallery ( userId ) ;
660+
661+ res . json ( {
662+ songs : songs . map ( song => ( {
663+ id : song . id ,
664+ clipId : song . clipId ,
665+ title : song . title || 'Untitled' ,
666+ status : song . status ,
667+ audioUrl : song . audioUrl ,
668+ imageUrl : song . imageUrl ,
669+ createdAt : song . createdAt . getTime ( ) ,
670+ completedAt : song . completedAt ? song . completedAt . getTime ( ) : null ,
671+ prompt : song . prompt ,
672+ tags : song . tags ,
673+ selectedPhotosCount : song . selectedPhotosCount ,
674+ selectedTranscriptionsCount : song . selectedTranscriptionsCount ,
675+ favorite : song . favorite || false ,
676+ metadata : song . metadata
677+ } ) )
678+ } ) ;
679+ } ) ;
680+
681+ // API endpoint to toggle song favorite status
682+ app . post ( '/api/songs/favorite' , ( req : any , res : any ) => {
683+ const userId = ( req as AuthenticatedRequest ) . authUserId ;
684+ const { songId, favorite } = req . body ;
685+
686+ if ( ! userId ) {
687+ res . status ( 401 ) . json ( { error : 'Not authenticated' } ) ;
688+ return ;
689+ }
690+
691+ const songs = this . getSongGallery ( userId ) ;
692+ const song = songs . find ( s => s . id === songId ) ;
693+
694+ if ( ! song ) {
695+ res . status ( 404 ) . json ( { error : 'Song not found' } ) ;
696+ return ;
697+ }
698+
699+ song . favorite = favorite ;
700+ res . json ( { success : true , favorite : song . favorite } ) ;
701+ } ) ;
702+
703+ // API endpoint to delete a song
704+ app . delete ( '/api/songs/:songId' , ( req : any , res : any ) => {
705+ const userId = ( req as AuthenticatedRequest ) . authUserId ;
706+ const songId = req . params . songId ;
707+
708+ if ( ! userId ) {
709+ res . status ( 401 ) . json ( { error : 'Not authenticated' } ) ;
710+ return ;
711+ }
712+
713+ const songs = this . getSongGallery ( userId ) ;
714+ const songIndex = songs . findIndex ( s => s . id === songId ) ;
715+
716+ if ( songIndex === - 1 ) {
717+ res . status ( 404 ) . json ( { error : 'Song not found' } ) ;
718+ return ;
719+ }
720+
721+ const song = songs [ songIndex ] ;
722+
723+ // Remove from active generations if still generating
724+ if ( this . activeGenerations . has ( song . clipId ) ) {
725+ this . activeGenerations . delete ( song . clipId ) ;
726+ }
727+
728+ // Remove from gallery
729+ songs . splice ( songIndex , 1 ) ;
730+
731+ res . json ( { success : true } ) ;
732+ } ) ;
733+
529734 // Main webview route - displays the enhanced interface
530735 app . get ( '/webview' , async ( req : any , res : any ) => {
531736 const userId = ( req as AuthenticatedRequest ) . authUserId ;
0 commit comments