This commit is contained in:
Thomas Demirdjian
2026-08-04 10:59:27 +02:00
parent 761f3cc200
commit c8656d6273
25 changed files with 1743 additions and 75 deletions
+245
View File
@@ -0,0 +1,245 @@
import * as ImagePicker from 'expo-image-picker'
import useMinuit from 'react-native-minuit/src/hooks/useMinuit.js'
import React, { useMemo, useState } from 'react'
import { Image, Text, View } from 'react-native'
import { background } from '../../assets'
import ActivityLoader from '../../components/ActivityLoader'
import BorderGradientButton from '../../components/BorderGradientButton'
import GradientButton from '../../components/GradientButton'
import MusicLandHeader from '../../components/MusicLandHeader'
import { getFunctionsClient } from '../../config/firebase'
import { uploadFileToFirebase } from '../../helpers/uploadToFirebase'
import Page from '../../layouts/Page'
import { Routes } from '../../navigation'
import { goBack, navigate } from '../../navigation/NavigationService'
import { useUserData } from '../../providers/UserDataProvider'
import { gutters, Palette } from '../../styles'
import { FONT_FAMILY } from '../../styles/Fonts'
import { PLAYBACK_AI_STATUS } from '../../utils/playbackAi'
import CreateLyricsHeader from '../Writing/components/CreateLyricsHeader'
const FUNCTIONS_REGION = 'europe-west1'
const trimString = (value) => (typeof value === 'string' ? value.trim() : '')
const isRemoteUrl = (value) => /^https?:\/\//i.test(trimString(value))
const resolveProject = ({ routeProject, selectedProject }) => {
if (routeProject?.id && selectedProject?.id === routeProject.id) {
return selectedProject
}
return routeProject || null
}
const getInitialPhotoUri = ({ routePhotoUrl, project }) =>
trimString(routePhotoUrl) || trimString(project?.playbackPhotoUrl) || null
const callStartPlaybackGeneration = (payload) => {
const callable = getFunctionsClient(FUNCTIONS_REGION).httpsCallable('heygen-startPlaybackGeneration')
return callable(payload)
}
const PlaybackAi = ({ route }) => {
const { project: routeProject, initialPhotoUrl = null } = route.params || {}
const { selectedProject, currentUID } = useUserData() || {}
const { setTooltip } = useMinuit()
const [selectedPhotoUri, setSelectedPhotoUri] = useState(() =>
getInitialPhotoUri({
routePhotoUrl: initialPhotoUrl,
project: resolveProject({ routeProject, selectedProject }),
})
)
const [isSubmitting, setIsSubmitting] = useState(false)
const project = useMemo(
() => resolveProject({ routeProject, selectedProject }),
[routeProject, selectedProject]
)
const handleSelectPhoto = async () => {
try {
const result = await ImagePicker.launchImageLibraryAsync({
mediaTypes: ImagePicker.MediaTypeOptions.Images,
allowsEditing: true,
aspect: [9, 16],
quality: 1,
})
const nextUri = result?.assets?.[0]?.uri || null
if (nextUri) {
setSelectedPhotoUri(nextUri)
}
} catch (error) {
setTooltip({
type: 'error',
text: error?.message || 'Impossible de sélectionner une photo',
})
}
}
const handleGenerate = async () => {
try {
if (!project?.id) {
throw new Error('Projet introuvable')
}
if (!currentUID) {
throw new Error('Utilisateur non authentifié')
}
if (!selectedPhotoUri) {
throw new Error('Choisis une photo avant de lancer la génération')
}
setIsSubmitting(true)
let photoUrl = trimString(selectedPhotoUri)
if (!isRemoteUrl(photoUrl)) {
const extension = trimString(selectedPhotoUri.split('.').pop()) || 'jpg'
const uploadResult = await uploadFileToFirebase({
uri: selectedPhotoUri,
path: `users/${currentUID}/projects/${project.id}/heygen/photo-${Date.now()}.${extension}`,
shouldCompress: true,
fileType: 'IMAGE',
})
photoUrl = trimString(uploadResult?.resultURI)
}
if (!photoUrl) {
throw new Error("Impossible d'envoyer la photo vers Firebase")
}
await callStartPlaybackGeneration({
projectId: project.id,
photoUrl,
})
navigate(Routes.PlaybackAiStatus, {
project: {
...project,
playbackStatus: PLAYBACK_AI_STATUS.GENERATING,
playbackGenerating: true,
playbackPhotoUrl: photoUrl,
},
})
} catch (error) {
setTooltip({
type: 'error',
text: error?.message || 'Erreur lors du lancement du playback IA',
})
} finally {
setIsSubmitting(false)
}
}
return (
<Page
backgroundImg={background.playbackBG2}
backgroundColor="#07111B"
headerType="NONE"
>
<MusicLandHeader progress={31} onPressBack={goBack} />
<View style={{ flex: 1, paddingBottom: gutters * 2 }}>
<CreateLyricsHeader title="Créer ton playback IA" />
<View style={styles.content}>
<View style={styles.summaryCard}>
<Text style={styles.summaryTitle}>{project?.title || 'Sans titre'}</Text>
<Text style={styles.summaryText}>
Ajoute une photo portrait de toi. HeyGen générera ensuite une vidéo verticale 9:16
tu chantes la musique de ton projet.
</Text>
</View>
<View style={styles.previewCard}>
{selectedPhotoUri ? (
<Image source={{ uri: selectedPhotoUri }} style={styles.photoPreview} resizeMode="cover" />
) : (
<View style={[styles.photoPreview, styles.photoPlaceholder]}>
<Text style={styles.photoPlaceholderText}>Aucune photo sélectionnée</Text>
</View>
)}
</View>
<View style={styles.actions}>
<BorderGradientButton
title={selectedPhotoUri ? 'Changer de photo' : 'Choisir une photo'}
onPress={handleSelectPhoto}
disabled={isSubmitting}
/>
<GradientButton
title="Générer ma vidéo"
onPress={handleGenerate}
disabled={isSubmitting || !selectedPhotoUri}
/>
{isSubmitting ? (
<ActivityLoader
defaultMessage="Envoi de la photo et lancement de HeyGen..."
containerStyle={styles.loader}
/>
) : null}
</View>
</View>
</View>
</Page>
)
}
const styles = {
content: {
flex: 1,
width: '90%',
alignSelf: 'center',
gap: 16,
},
summaryCard: {
padding: 18,
borderRadius: 18,
borderWidth: 1,
borderColor: 'rgba(255,255,255,0.14)',
backgroundColor: 'rgba(5,12,24,0.76)',
gap: 8,
},
summaryTitle: {
color: Palette.white,
fontSize: 18,
fontFamily: FONT_FAMILY.InterSemiBold,
},
summaryText: {
color: Palette.white,
opacity: 0.78,
lineHeight: 20,
fontFamily: FONT_FAMILY.InterRegular,
},
previewCard: {
flex: 1,
minHeight: 320,
borderRadius: 22,
borderWidth: 1,
borderColor: 'rgba(255,255,255,0.14)',
backgroundColor: 'rgba(5,12,24,0.8)',
padding: 12,
},
photoPreview: {
width: '100%',
height: '100%',
borderRadius: 18,
backgroundColor: '#141414',
},
photoPlaceholder: {
alignItems: 'center',
justifyContent: 'center',
},
photoPlaceholderText: {
color: Palette.white,
opacity: 0.5,
fontFamily: FONT_FAMILY.InterRegular,
},
actions: {
gap: 12,
},
loader: {
marginTop: 4,
},
}
export default PlaybackAi