From 11b5ef0ad2b6eb3f30c3d4cde9b54bd6927aef4c Mon Sep 17 00:00:00 2001 From: R0m1k3 Date: Thu, 12 Feb 2026 10:08:46 +0100 Subject: [PATCH] =?UTF-8?q?feat(mobile-reel):=20suppression=20bouton=20cam?= =?UTF-8?q?=C3=A9ra,=20am=C3=A9lioration=20sync=20TTS=20(chunks=203=20mots?= =?UTF-8?q?=20+=20overlap)=20et=20style=20sous-titres=20(outline/shadow)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/pages/mobile/new-reel.tsx | 46 +++------------------------- ffmpeg-service/main.py | 16 ++++++---- 2 files changed, 14 insertions(+), 48 deletions(-) diff --git a/client/src/pages/mobile/new-reel.tsx b/client/src/pages/mobile/new-reel.tsx index 6af80e8..30b2a75 100644 --- a/client/src/pages/mobile/new-reel.tsx +++ b/client/src/pages/mobile/new-reel.tsx @@ -1,10 +1,9 @@ -import { useState, useCallback, useRef, useMemo } from "react"; -import { CameraRecorder } from "@/components/camera-recorder"; +import { useState, useCallback, useRef } from "react"; import { useQuery, useMutation } from "@tanstack/react-query"; import { useLocation } from "wouter"; import { Send, Sparkles, Video, Music, Type, Calendar, - Upload, Camera, Play, Pause, Volume2, + Upload, Play, Pause, Volume2, ChevronRight, Loader2, Check, RefreshCw, ChevronLeft, Menu, Mic } from "lucide-react"; @@ -41,27 +40,8 @@ type Step = 'video' | 'music' | 'text' | 'publish'; export default function MobileNewReel() { const [, navigate] = useLocation(); const { toast } = useToast(); - const cameraInputRef = useRef(null); const audioRef = useRef(null); - const [showCamera, setShowCamera] = useState(false); - - // Détection iOS pour utiliser la caméra native (meilleure qualité + stabilisation) - const isIOS = useMemo(() => { - if (typeof navigator === 'undefined') return false; - return /iPad|iPhone|iPod/.test(navigator.userAgent) || - (navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1); - }, []); - - // Handler pour le bouton caméra - iOS utilise l'input natif, Android utilise WebRTC - const handleCameraButtonClick = useCallback(() => { - if (isIOS) { - cameraInputRef.current?.click(); - } else { - setShowCamera(true); - } - }, [isIOS]); - // État du workflow const [currentStep, setCurrentStep] = useState('video'); @@ -192,10 +172,7 @@ export default function MobileNewReel() { noKeyboard: true, }); - const handleCameraCapture = (e: React.ChangeEvent) => { - const file = e.target.files?.[0]; - if (file) uploadMutation.mutate(file); - }; + const togglePlayPreview = async (track: MusicTrack) => { if (isPlaying === track.id) { @@ -274,28 +251,13 @@ export default function MobileNewReel() {
- -
- +
- {/* CameraRecorder pour Android uniquement */} - {showCamera && !isIOS && ( - { - uploadMutation.mutate(file); - setShowCamera(false); - }} - onCancel={() => setShowCamera(false)} - /> - )} - {selectedVideo ? (
diff --git a/ffmpeg-service/main.py b/ffmpeg-service/main.py index 6b8fc1c..764634f 100644 --- a/ffmpeg-service/main.py +++ b/ffmpeg-service/main.py @@ -334,9 +334,9 @@ def generate_ass_from_word_boundaries( for i, wb in enumerate(word_boundaries): current_words.append(wb["text"]) is_last = i == len(word_boundaries) - 1 - # Split at punctuation or every 5 words + # Split at punctuation or every 3 words (tighter sync with voice) ends_sentence = wb["text"].rstrip().endswith((".", "!", "?", ":", ",")) - at_limit = len(current_words) >= 5 + at_limit = len(current_words) >= 3 if is_last or ends_sentence or at_limit: # End time = this word's offset + its duration @@ -358,6 +358,10 @@ def generate_ass_from_word_boundaries( if total_duration and chunks: chunks[-1]["end"] = max(chunks[-1]["end"], total_duration) + # Add 50ms overlap between consecutive chunks to prevent flickering + for i in range(len(chunks) - 1): + chunks[i]["end"] = max(chunks[i]["end"], chunks[i + 1]["start"] + 0.05) + # ASS Header header = f"""[Script Info] ScriptType: v4.00+ @@ -367,7 +371,7 @@ ScaledBorderAndShadow: yes [V4+ Styles] Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding -Style: Default,Sans,{font_size},&H00FFFFFF,&H000000FF,&H00000000,&H80000000,-1,0,0,0,100,100,0,0,1,2,0,5,50,50,0,1 +Style: Default,Sans,{font_size},&H00FFFFFF,&H000000FF,&H00000000,&H80000000,-1,0,0,0,100,100,0,0,1,4,2,5,50,50,0,1 [Events] Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text @@ -400,7 +404,7 @@ def generate_simple_ass( for word in words: current_chunk.append(word) - if len(current_chunk) >= 5 or word.endswith((".", "!", "?", ":")): + if len(current_chunk) >= 3 or word.endswith((".", "!", "?", ":")): chunks.append(" ".join(current_chunk)) current_chunk = [] @@ -418,7 +422,7 @@ ScaledBorderAndShadow: yes [V4+ Styles] Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding -Style: Default,Sans,{font_size},&H0000FFFF,&H00FFFFFF,&H00000000,&H80000000,-1,0,0,0,100,100,0,0,1,3,1,5,50,50,0,1 +Style: Default,Sans,{font_size},&H0000FFFF,&H00FFFFFF,&H00000000,&H80000000,-1,0,0,0,100,100,0,0,1,4,2,5,50,50,0,1 [Events] Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text @@ -521,7 +525,7 @@ ScaledBorderAndShadow: yes [V4+ Styles] Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding -Style: Default,Sans,{font_size},&H0000FFFF,&H00FFFFFF,&H00000000,&H80000000,-1,0,0,0,100,100,0,0,1,3,1,5,50,50,0,1 +Style: Default,Sans,{font_size},&H0000FFFF,&H00FFFFFF,&H00000000,&H80000000,-1,0,0,0,100,100,0,0,1,4,2,5,50,50,0,1 [Events] Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text