Coverage for src/code_beatrix/art/video.py: 78%

381 statements  

« prev     ^ index     » next       coverage.py v7.1.0, created at 2023-04-29 13:45 +0200

1# -*- coding: utf-8 -*- 

2""" 

3@file 

4@brief Quelques questions d'ordre général autour du langage Python. 

5""" 

6from contextlib import redirect_stdout, redirect_stderr 

7import io 

8import os 

9import sys 

10import tempfile 

11import time 

12import numpy 

13from pytube import YouTube # pylint: disable=E0401 

14from pytube.exceptions import RegexMatchError # pylint: disable=E0401 

15from imageio import imsave 

16import moviepy.audio.fx.all as afx 

17import moviepy.video.fx.all as vfx 

18from moviepy.video.VideoClip import ImageClip, VideoClip 

19from moviepy.video.io.ImageSequenceClip import ImageSequenceClip 

20from moviepy.audio.AudioClip import CompositeAudioClip 

21from moviepy.video.compositing.CompositeVideoClip import CompositeVideoClip 

22from moviepy.video.compositing.concatenate import concatenate_videoclips 

23from moviepy.audio.AudioClip import concatenate_audioclips, AudioArrayClip 

24from PIL import Image, ImageFont, ImageDraw 

25from .moviepy_context import AudioContext, VideoContext, get_wrapped, clean_video 

26 

27 

28class FontError(Exception): 

29 """ 

30 Raised when a font cannot be found. 

31 """ 

32 pass 

33 

34 

35def check(fLOG=None): 

36 """ 

37 Checks a couple of functionality works. 

38 The test takes 5-6 seconds to download, 

39 4-5 seconds to process the video. 

40 

41 @param logging function 

42 """ 

43 t1 = time.perf_counter() 

44 with tempfile.TemporaryDirectory() as temp: 

45 if fLOG: 

46 fLOG('[check] download_youtube_video') 

47 vid = download_youtube_video("4o5baMYWdtQ", temp, res=None) 

48 vid = os.path.join(temp, vid) 

49 t2 = time.perf_counter() 

50 if fLOG: 

51 fLOG('[check] video_compose') 

52 ext = video_compose(vid, vid, t2=2, place="h2") 

53 dest = os.path.join(temp, "res.mp4") 

54 if fLOG: 

55 fLOG('[check] video_save') 

56 video_save(ext, dest) 

57 res = os.path.exists(dest) 

58 delta1 = time.perf_counter() - t1 

59 delta2 = time.perf_counter() - t2 

60 if fLOG: 

61 fLOG("[check] video time={0} - video={1}".format(delta1, delta2)) 

62 return res 

63 

64 

65########## 

66# youtube 

67########## 

68 

69 

70def download_youtube_video(tag, output_path=None, res='720p', mime_type="video/mp4", **kwargs): 

71 """ 

72 Downloads a video from :epkg:`youtube` with :epkg:`pytube`. 

73 Télécharge une vidéo depuis :epkg:`youtube` avec :epkg:`pytube`. 

74 

75 @param tag tag of the :epkg:`youtube` video to download 

76 @param output_path output path 

77 @param mime_type see :epkg:`youtube` 

78 @param res see :epkg:`youtube` 

79 @param kwargs see :epkg:`youtube` 

80 @return filename (relative to *output_path*) 

81 

82 .. faqref:: 

83 :title: Télécharger une vidéo sur YouTube 

84 

85 Le module :epkg:`pytube` permet de télécharger une vidéo 

86 :epkg:`youtube`. Chaque vidéo est disponible selon plusieurs 

87 format dont on récupère la liste avant de choisir 

88 qui correspond à celui voulu. 

89 

90 :: 

91 

92 from pytube import YouTube 

93 yt = YouTube('https://www.youtube.com/watch?v=tRFHXMQP-QU') 

94 st = yt.streams 

95 fil = st.filter(mime_type="video/mp4", res="720p") 

96 fil.first().download() 

97 

98 """ 

99 url = 'https://www.youtube.com/watch?v={0}'.format(tag) 

100 try: 

101 yt = YouTube(url) 

102 except RegexMatchError as e: 

103 raise RuntimeError( 

104 "Unable to process tag=%r (url=%r)" % (tag, url)) from e 

105 st = yt.streams.filter(mime_type=mime_type, res=res, **kwargs) 

106 fi = st.first() 

107 if fi is None: 

108 raise ValueError( 

109 "By default the function downloads a video with resolution = 720, " 

110 "if it is not available, switch to res=None " 

111 "to choose the first one available [tag=%r url=%r]" % (tag, url)) 

112 fi.download(output_path=output_path) 

113 return fi.default_filename 

114 

115######## 

116# audio 

117######## 

118 

119 

120def audio_extract_audio(audio_or_file, ta=0, tb=None): 

121 """ 

122 Extracts a part of an audio. 

123 Extrait une partie du son. 

124 Uses `subclip <https://zulko.github.io/moviepy/ref/AudioClip.html?highlight=audioclip#moviepy.audio.AudioClip.AudioClip.subclip>`_. 

125 

126 @param audio_or_file string or :epkg:`AudioClip` 

127 @param ta beginning 

128 @param tb end 

129 @return :epkg:`VideoClip` 

130 

131 Example: 

132 

133 :: 

134 

135 from code_beatrix.art.video import audio_extract_audio 

136 son = audio_extract_audio('son.mp3', '00:00:01', '00:00:02') 

137 """ 

138 with AudioContext(audio_or_file) as audio: 

139 return audio.subclip(ta, tb) 

140 

141 

142def audio_save(audio_or_file, filename, verbose=False, **kwargs): 

143 """ 

144 Saves as a sound. 

145 Enregistre un son dans un fichier. 

146 Uses `write_audiofile <https://zulko.github.io/moviepy/ref/AudioClip.html? 

147 highlight=audioclip#moviepy.audio.AudioClip.AudioClip.write_audiofile>`_. 

148 

149 @param audio_or_file string or :epkg:`AudioClip` 

150 @param filename save into this filename 

151 @param verbose logging or not 

152 @param kwargs see `write_audiofile <https://zulko.github.io/moviepy/ref/ 

153 VideoClip/VideoClip.html?highlight=videofileclip#moviepy.video. 

154 io.VideoFileClip.VideoFileClip.write_videofile>`_ 

155 """ 

156 with AudioContext(audio_or_file) as audio: 

157 if verbose: 

158 audio.write_audiofile(filename, verbose=verbose, **kwargs) 

159 else: 

160 f = io.StringIO() 

161 with redirect_stdout(f): 

162 with redirect_stderr(f): 

163 audio.write_audiofile(filename, verbose=verbose, **kwargs) 

164 

165 

166def audio_modification(audio, loop_duration=None, volumex=1., 

167 fadein=False, fadeout=False, t_start=0, t_end=None, 

168 speed=1., keep_duration=False, wav=False): 

169 """ 

170 Modifies a sound. 

171 Modifie un son. 

172 

173 @param audio sound 

174 @param loop_duration loops sound 

175 @param volumex multiplies the sound 

176 @param fadein decreases the volume of the first seconds 

177 @param fadeout decreases the volume of the last seconds 

178 @param t_start shorten the audio 

179 @param t_end shorten the audio 

180 @param speed speed of the sound 

181 @param keep_duration parameter to 

182 `ft_time <https://zulko.github.io/moviepy/ref/AudioClip.html? 

183 highlight=fl_time#moviepy.audio.AudioClip.AudioClip.fl_time>`_ 

184 @return new sound 

185 """ 

186 with AudioContext(audio) as audio_: 

187 if loop_duration: 

188 if audio_.duration is None: 

189 raise ValueError( 

190 "The duration is unknown, maybe you should apply the loop first.") 

191 audio_ = afx.audio_loop(audio_, duration=loop_duration) 

192 if volumex != 1.: 

193 audio_ = audio_.fx(afx.volumex, volumex) 

194 if speed != 1.: 

195 audio_ = audio_.fl_time(lambda t: t * speed, 

196 keep_duration=keep_duration) 

197 if fadein: 

198 audio_ = audio_.fx(afx.audio_fadein, 1.0) 

199 if fadeout: 

200 audio_ = audio_.fx(afx.audio_fadeout, 1.0) 

201 if t_start != 0 or t_end is not None: 

202 audio_ = audio_.subclip(t_start=t_start, t_end=t_end) 

203 return audio_ 

204 

205 

206def audio2wav(audio, duration=None, **kwargs): 

207 """ 

208 The sound is converted into :epkg:`wav` 

209 and returned as an :epkg:`AudioArrayClip`. 

210 Le son est converti au format :epkg:`wav`. 

211 

212 @param audio sound 

213 @param duration change the duration of the sound before converting it 

214 @param kwargs see `to_soundarray <https://zulko.github.io/moviepy/ref/AudioClip.html? 

215 highlight=to_soundarray#moviepy.audio.AudioClip.AudioClip.to_soundarray>`_ 

216 @return :epkg:`AudioArrayClip` 

217 """ 

218 with AudioContext(audio) as audio_: 

219 if duration is not None: 

220 audio_ = audio_.set_duration(duration) 

221 wav = audio_.to_soundarray(**kwargs) 

222 fps = kwargs.get('fps', audio_.fps if hasattr(audio_, 'fps') else None) 

223 if fps is None: 

224 raise ValueError("fps cannot be None, 44100 is a proper value") 

225 return AudioArrayClip(wav, fps=fps) 

226 

227 

228def audio_compose(audio_or_file1, audio_or_file2, t1=0, t2=None): 

229 """ 

230 Concatenates or superposes two sounds. 

231 Ajoute ou superpose deux sons. 

232 

233 @param audio_or_file1 son 1 

234 @param audio_or_file2 son 2 

235 @param t1 start of the first sound 

236 @param t2 start of the second sound (or None to add it ad 

237 @return new sound 

238 

239 Example: 

240 

241 :: 

242 

243 from code_beatrix.art.video import audio_compose 

244 son = audio_compose('son1.mp3', 'son2.mp3', 0, 10) 

245 """ 

246 with AudioContext(audio_or_file1) as audio1: 

247 with AudioContext(audio_or_file2) as audio2: 

248 add = [] 

249 if t1 != 0: 

250 add.append(audio1.set_start(t1)) 

251 else: 

252 add.append(audio1) 

253 if t2 is None: 

254 add.append(audio2.set_start(audio1.duration + t1)) 

255 else: 

256 add.append(audio2.set_start(t2)) 

257 comp = CompositeAudioClip(add) 

258 fps1 = audio1.fps if hasattr(audio1, 'fps') else None 

259 fps2 = audio2.fps if hasattr(audio2, 'fps') else None 

260 if fps1 is not None and fps2 is not None: 

261 fps = max(fps1, fps2) 

262 return comp.set_fps(fps) 

263 elif fps1 is None and fps2 is None: 

264 return comp 

265 else: 

266 return comp.set_fps(fps1 or fps2) 

267 

268 

269def audio_concatenate(audio_or_files, **kwargs): 

270 """ 

271 Concatenates sounds. 

272 Met bout à bout des sons. 

273 

274 @param audio_or_files list of sounds or filenames 

275 @param kwargs additional parameters for 

276 `concatenate_audioclips <https://github.com/Zulko/moviepy/blob/master/moviepy/audio/AudioClip.py#L308>`_ 

277 @return :epkg:`AudioClip` 

278 

279 Example: 

280 

281 :: 

282 

283 from code_beatrix.art.video import audio_concatenate 

284 son = audio_concatenate('son1.mp3', 'son2.mp3') 

285 """ 

286 ctx = [AudioContext(_).__enter__() for _ in audio_or_files] 

287 res = concatenate_audioclips([get_wrapped(_) for _ in ctx], **kwargs) 

288 for _ in ctx: 

289 _.__exit__() 

290 return res 

291 

292######## 

293# vidéo 

294######## 

295 

296 

297def video_extract_video(video_or_file, ta=0, tb=None): 

298 """ 

299 Extracts a part of a video. 

300 Extrait une partie de la vidéo. 

301 Uses `subclip <https://zulko.github.io/moviepy/ref/VideoClip/VideoClip.html? 

302 highlight=videofileclip#moviepy.video.VideoClip.VideoClip.subclip>`_. 

303 

304 @param video_or_file string or :epkg:`VideoClip` 

305 @param ta beginning 

306 @param tb end 

307 @return :epkg:`VideoClip` 

308 

309 Example: 

310 

311 :: 

312 

313 from code_beatrix.faq_faq_video import video_extract_video 

314 vid = video_extract_video('exemple.mp4', '00:00:01', '00:00:04') 

315 """ 

316 with VideoContext(video_or_file) as video: 

317 return video.subclip(ta, tb) 

318 

319 

320def video_load(video_or_file): 

321 """ 

322 Loads a video. 

323 Charge une vidéo. 

324 

325 @param video_or_file string or :epkg:`VideoClip` 

326 @return :epkg:`VideoClip` 

327 """ 

328 with VideoContext(video_or_file) as video: 

329 return video.video 

330 

331 

332def video_save_image(video_or_file, t=None, filename=None, **kwargs): 

333 """ 

334 Saves one image from a video. 

335 Enregistre une image extraite d'une vidéo. 

336 

337 

338 @param video_or_file string or :epkg:`VideoClip` 

339 @param filename if not None, saves the image into this file 

340 @param kwargs see `save_frame <https://zulko.github.io/moviepy/ref/VideoClip/ 

341 VideoClip.html?highlight=save_frame#moviepy.video.io.VideoFileClip.VideoFileClip.save_frame>`_ 

342 @return one image if *filename* is None 

343 

344 Example: 

345 

346 :: 

347 

348 from code_beatrix.faq_faq_video import video_extract_video, video_save_image 

349 vid = video_extract_video('exemple.mp4', '00:00:01', '00:00:04') 

350 video_save_image(vid, filename='new_image.jpg', t=2) 

351 """ 

352 with VideoContext(video_or_file) as video: 

353 if filename is not None: 

354 video.save_frame(filename, t=t, **kwargs) 

355 return filename 

356 else: 

357 im = video.get_frame(t) 

358 if kwargs.get('withmask', True) and video.mask is not None: 

359 mask = 255 * video.mask.get_frame(t) 

360 im = numpy.dstack([im, mask]).astype('uint8') 

361 return Image.fromarray(im) 

362 else: 

363 return Image.fromarray(im).convert('RGBA') 

364 

365 

366def video_save(video_or_file, filename, verbose=False, duration=None, **kwargs): 

367 """ 

368 Saves as a video or as a :epkg:`gif`. 

369 Enregistre une vidéo dans un fichier. 

370 Uses `write_videofile <https://zulko.github.io/moviepy/ref/VideoClip/VideoClip.html? 

371 highlight=videofileclip#moviepy.video.io.VideoFileClip.VideoFileClip.write_videofile>`_. 

372 

373 @param video_or_file string or :epkg:`VideoClip` 

374 @param filename video saved into this filename 

375 @param duration overwrite duration, 

376 see method `set_duration <https://zulko.github.io/moviepy/ref/VideoClip/VideoClip.html? 

377 highlight=videoclip#moviepy.video.VideoClip.VideoClip.set_duration>`_ 

378 @param verbose logging or not 

379 @param kwargs see `write_videofile <https://zulko.github.io/moviepy/ref/ 

380 VideoClip/VideoClip.html?highlight=videofileclip#moviepy.video.io. 

381 VideoFileClip.VideoFileClip.write_videofile>`_ 

382 

383 Example: 

384 

385 :: 

386 

387 from code_beatrix.faq_faq_video import video_extract_video, video_save 

388 vid = video_extract_video('exemple.mp4', '00:00:01', '00:00:04') 

389 video_save(vid, 'new_video.mp4') 

390 """ 

391 if isinstance(filename, str) and os.path.splitext(filename)[-1] == '.gif': 

392 with VideoContext(video_or_file) as video: 

393 if duration is not None: 

394 video = video.set_duration(duration) 

395 if verbose: 

396 video.write_gif(filename, verbose=verbose, **kwargs) 

397 else: 

398 f = io.StringIO() 

399 with redirect_stdout(f): 

400 with redirect_stderr(f): 

401 video.write_gif(filename, verbose=verbose, **kwargs) 

402 else: 

403 with VideoContext(video_or_file) as video: 

404 if duration is not None: 

405 video = video.set_duration(duration) 

406 if verbose: 

407 video.write_videofile(filename, verbose=verbose, **kwargs) 

408 else: 

409 f = io.StringIO() 

410 with redirect_stdout(f): 

411 with redirect_stderr(f): 

412 video.write_videofile( 

413 filename, verbose=verbose, **kwargs) 

414 

415 

416def video_enumerate_frames(video_or_file, folder=None, fps=10, pattern='images_%04d.jpg', 

417 clean=False, **kwargs): 

418 """ 

419 Enumerates frames from a video. 

420 Itère sur des images depuis une vidéo. 

421 

422 @param video_or_file string or :epkg:`VideoClip` 

423 @param folder where to exports the images or returns arrays if None 

424 @param pattern image names 

425 @param fps frames per seconds 

426 @param clean clean open processes after it is done 

427 @param kwargs arguments to `iter_frames <https://zulko.github.io/moviepy/ref/ 

428 AudioClip.html?highlight=frames#moviepy.audio.AudioClip.AudioClip.iter_frames>`_ 

429 @return iterator on arrays or files (see parameter *folder*) 

430 

431 Example: 

432 

433 :: 

434 

435 form code_beatrix.art.video import video_enumerate_frames 

436 vid = 'example.mp4') 

437 for frame in video_enumerate_frames(vid, folder=temp): 

438 # ... 

439 

440 If *clean* is true, it calls @see fn clean_video. 

441 """ 

442 if clean is None: 

443 raise ValueError('cannot be None') 

444 with VideoContext(video_or_file) as video: 

445 if folder is None: 

446 for frame in video.iter_frames(fps=fps, **kwargs): 

447 yield frame 

448 if clean: 

449 clean_video(video.video) 

450 else: 

451 if 'dtype' in kwargs: 

452 if kwargs['dtype'] != 'uint8': 

453 raise ValueError("dtype must be uint8") 

454 del kwargs['dtype'] 

455 

456 for i, frame in enumerate(video.iter_frames(fps=fps, dtype='uint8', **kwargs)): 

457 # saves as image 

458 name = os.path.join(folder, pattern % i) 

459 imsave(name, frame) 

460 yield name 

461 if clean: 

462 clean_video(video.video) 

463 

464 

465def video_replace_audio(video_or_file, new_sound, loop=True): 

466 """ 

467 Replaces the sound of a video. 

468 Remplace la bande-son d'une vidéo. 

469 

470 @param video_or_file string or :epkg:`VideoClip` 

471 @param new_sound sound 

472 @param loop loop on the audio if not long enough 

473 @return :epkg:`VideoClip` 

474 

475 The list of available transformations is at: 

476 `vfx <https://zulko.github.io/moviepy/ref/videofx.html?highlight=vfx>`_. 

477 If parameter ``loop=True`` is specified, 

478 *loop_duration* becomes the duration of the video. 

479 

480 Example: 

481 

482 :: 

483 

484 from code_beatrix.art.video import video_replace_sound 

485 vid = video_replace_sound('video.mp4', 'son.mp3', loop=True, volumex=5.5, t_end='00:00:05') 

486 """ 

487 with VideoContext(video_or_file) as video: 

488 if loop: 

489 if video.duration is None: 

490 raise ValueError( 

491 "The duration of the video is unknown, use audio_modification and loop=False") 

492 audio = audio_modification(new_sound, loop_duration=video.duration) 

493 else: 

494 audio = new_sound 

495 new_clip = video.set_audio(audio) 

496 return new_clip 

497 

498 

499def video_extract_audio(video_or_file): 

500 """ 

501 Returns the audio of a video. 

502 Retourne le son d'une vidéo. 

503 

504 @param video_or_file string or :epkg:`VideoClip` 

505 @return :epkg:`AudioClip` 

506 """ 

507 with VideoContext(video_or_file) as video: 

508 return video.audio 

509 

510 

511def video_remove_audio(video_or_file): 

512 """ 

513 Returns the same video without audio. 

514 Retourne la même vidéo sans le son. 

515 

516 @param video_or_file string or :epkg:`VideoClip` 

517 @return :epkg:`AudioClip` 

518 """ 

519 with VideoContext(video_or_file) as video: 

520 return video.without_audio() 

521 

522 

523def video_compose(video_or_file1, video_or_file2=None, t1=0, t2=0, place=None, **kwargs): 

524 """ 

525 Concatenates or superposes two videos. 

526 Ajoute ou superpose deux vidéos. 

527 

528 @param video_or_file1 vidéo 1 or list of video 

529 @param video_or_file2 vidéo 2 

530 @param t1 start of the first sound 

531 @param t2 start of the second sound (or None to add it ad 

532 @param place predefined placements 

533 @param kwargs additional parameters, 

534 sent to `CompositeVideoClip <https://zulko.github.io/moviepy/ref/ 

535 VideoClip/VideoClip.html?highlight=compositevideoclip#compositevideoclip>`_ 

536 @return :epkg:`VideoClip` 

537 

538 Example: 

539 

540 :: 

541 

542 from code_beatrix.art.video import video_compose 

543 vid = video_compose('video1.mp4', 'video2.mp4', '00:00:01', '00:00:04') 

544 

545 The first video defines the size of the final video. 

546 List of predefined placements: 

547 

548 * *h2*: two videos side by side horizontally 

549 * *v2*: two videos side by side vertically 

550 * *br*: two videos, second is placed at the bottom right corner 

551 

552 *zoom* can be defined as a argument, it applies on the second 

553 video if *place* is defined and if there are two videos. 

554 """ 

555 if place is None: 

556 if isinstance(video_or_file1, list): 

557 if video_or_file2 is not None: 

558 raise ValueError( 

559 'video_or_file1 is a list, video_or_file2 should be None') 

560 vids = [VideoContext(i).__enter__() for i in video_or_file1] 

561 comp = [] 

562 for i, v in enumerate(vids): 

563 v = v.video 

564 if isinstance(t1, list) and i < len(t1): 

565 v.set_start(t1[i]) 

566 comp.append(v) 

567 res = CompositeVideoClip(comp, **kwargs) 

568 for v in vids: 

569 v.__exit__() 

570 return res 

571 else: 

572 with VideoContext(video_or_file1) as video1: 

573 with VideoContext(video_or_file2) as video2: 

574 add = [] 

575 if t1 != 0: 

576 add.append(video1.set_start(t1)) 

577 else: 

578 add.append(video1) 

579 if t2 is None: 

580 add.append(video2.set_start(video1.duration + t1)) 

581 else: 

582 add.append(video2.set_start(t2)) 

583 return CompositeVideoClip(add, **kwargs) 

584 else: 

585 

586 def get_two(video_or_file1, video_or_file2, t1, t2): 

587 if isinstance(video_or_file1, list): 

588 if len(video_or_file1) != 2: 

589 raise ValueError( 

590 "Expecting two videos not {0}".format(len(video_or_file1))) 

591 v1, v2 = video_or_file1 

592 t1, t2 = t1 

593 else: 

594 if video_or_file2 is None: 

595 raise ValueError("Expecting two videos not less") 

596 v1, v2 = video_or_file1, video_or_file2 

597 vc1 = VideoContext(v1).__enter__() 

598 vc2 = VideoContext(v2).__enter__() 

599 return (vc1, vc2), (t1, t2) 

600 

601 (vc1, vc2), (t1, t2) = get_two(video_or_file1, video_or_file2, t1, t2) 

602 

603 v1 = vc1.video 

604 v2 = vc2.video 

605 

606 if kwargs.get('zoom', 1.) != 1.: 

607 v2 = video_modification(v2, resize=kwargs['zoom']) 

608 del kwargs['zoom'] 

609 

610 # Predefined placements. 

611 if place == "h2": 

612 pos1 = 0, 0 

613 pos2 = v1.size[0], 0 

614 v1 = video_position(v1, pos=pos1) 

615 v2 = video_position(v2, pos=pos2) 

616 if 'size' not in kwargs: 

617 kwargs['size'] = v1.size[0] + \ 

618 v2.size[0], max(v1.size[1], v2.size[1]) 

619 res = video_compose(v1, v2, t1, t2, **kwargs) 

620 elif place == "v2": 

621 pos1 = 0, 0 

622 pos2 = 0, v1.size[1] 

623 v1 = video_position(v1, pos=pos1) 

624 v2 = video_position(v2, pos=pos2) 

625 if 'size' not in kwargs: 

626 kwargs['size'] = max(v1.size[0], v2.size[0] 

627 ), v1.size[1] + v2.size[1] 

628 res = video_compose(v1, v2, t1, t2, **kwargs) 

629 elif place == "br": 

630 pos1 = 0, 0 

631 pos2 = max(0, v1.size[0] - v2.size[0] 

632 ), max(0, v1.size[1] - v2.size[1]) 

633 v1 = video_position(v1, pos=pos1) 

634 v2 = video_position(v2, pos=pos2) 

635 if 'size' not in kwargs: 

636 kwargs['size'] = v1.size[0], v1.size[1] 

637 res = video_compose(v1, v2, t1, t2, **kwargs) 

638 else: 

639 raise ValueError("Unknown placement '{0}'".format(place)) 

640 

641 vc1.__exit__() 

642 vc2.__exit__() 

643 return res 

644 

645 

646def video_concatenate(video_or_files, **kwargs): 

647 """ 

648 Concatenates videos. 

649 Met bout à bout des vidéos. 

650 

651 @param video_or_files list of videos or filenames 

652 @param kwargs additional parameters for 

653 `concatenate_videoclips <https://github.com/Zulko/moviepy/blob/master/ 

654 moviepy/video/compositing/concatenate.py#L15>`_ 

655 @return :epkg:`VideoClip` 

656 """ 

657 ctx = [VideoContext(_).__enter__() for _ in video_or_files] 

658 res = concatenate_videoclips([get_wrapped(_) for _ in ctx], **kwargs) 

659 for _ in ctx: 

660 _.__exit__() 

661 return res 

662 

663 

664def video_modification(video_or_file, volumex=1., resize=1., speed=1., 

665 mirrorx=False, mirrory=False): 

666 """ 

667 Modifies a video. 

668 Modifie une vidéo. 

669 

670 @param video_or_file string or :epkg:`VideoClip` 

671 @param volumex multiplies the sound 

672 @param speed speed of the sound 

673 @param resize resize 

674 @param mirrorx mirror x 

675 @param mirrory mirror y 

676 @return new video 

677 

678 Example: 

679 

680 :: 

681 

682 from code_beatrix.art.video import video_modification 

683 vid = video_modification('video.mp4', speed=2., mirrory=True, mirrorx=True) 

684 """ 

685 def check_duration(video): 

686 if video.duration is None: 

687 raise ValueError('video duration should not be None') 

688 

689 with VideoContext(video_or_file) as video: 

690 if speed: 

691 check_duration(video) 

692 dur = video.duration 

693 video = video.fl_time(lambda t: t * speed) 

694 video = video.set_duration(dur / speed) 

695 if volumex != 1.: 

696 video = video.fx(vfx.volumex, volumex) 

697 if resize != 1.: 

698 video = video.fx(vfx.resize, resize) 

699 if mirrorx: 

700 video = video.fx(vfx.mirror_x) 

701 if mirrory: 

702 video = video.fx(vfx.mirror_y) 

703 return video 

704 

705 

706def video_image(image_or_file, duration=None, zoom=None, opacity=None, **kwargs): 

707 """ 

708 Creates a :epkg:`ImageClip`. 

709 Créé une vidéo à partir d'une image. 

710 

711 @param image_or_file image or file 

712 @param duration duration or None if not known 

713 @param zoom applies a zoom on the image 

714 @param opacity opacity of the image (0 for transparent, 255 for opaque) 

715 @param kwargs additional parameters for :epkg:`ImageClip` 

716 @return :epkg:`ImageClip` 

717 

718 If *duration* is None, it will be fixed when the image is 

719 composed with another one. The image remains wherever it is placed. 

720 """ 

721 if isinstance(image_or_file, str): 

722 img = Image.open(image_or_file) 

723 return video_image(img, duration=duration, zoom=zoom, opacity=opacity, **kwargs) 

724 elif isinstance(image_or_file, numpy.ndarray): 

725 if zoom is not None: 

726 from skimage.transform import rescale 

727 img = rescale(image_or_file, zoom) 

728 return video_image(img, duration=duration, opacity=opacity, **kwargs) 

729 else: 

730 img = image_or_file 

731 if len(img.shape) != 3: 

732 raise ValueError( 

733 "Image is not RGB or RGBA shape={0}".format(img.shape)) 

734 if img.shape[2] == 3: 

735 from skimage.io._plugins.pil_plugin import pil_to_ndarray 

736 pilimg = Image.fromarray(img).convert('RGBA') 

737 img = pil_to_ndarray(pilimg) 

738 if opacity is None: 

739 opacity = 255 

740 if isinstance(opacity, int): 

741 img[:, :, 3] = opacity 

742 elif isinstance(opacity, float): 

743 img[:, :, 3] = int(opacity * 255) 

744 elif opacity is not None: 

745 raise TypeError("opacity should be int or float or None") 

746 return ImageClip(img, duration=duration, transparent=True, **kwargs) 

747 elif isinstance(image_or_file, Image.Image): 

748 from skimage.io._plugins.pil_plugin import pil_to_ndarray 

749 if image_or_file.mode != 'RGBA': 

750 image_or_file = image_or_file.convert('RGBA') 

751 if zoom is not None: 

752 image_or_file = image_or_file.resize(zoom) 

753 img = pil_to_ndarray(image_or_file) 

754 return video_image(img, duration=duration, opacity=opacity, **kwargs) 

755 else: 

756 raise TypeError( 

757 "Unable to create a video from type {0}".format(type(image_or_file))) 

758 

759 

760def video_position(video_or_file, pos, relative=False): 

761 """ 

762 Modifies the position of a position. 

763 Modifie la position d'une video. 

764 Relies on function 

765 `set_position <https://zulko.github.io/moviepy/ref/VideoClip/VideoClip.html? 

766 highlight=imageclip#moviepy.video.VideoClip.VideoClip.set_position>`_. 

767 

768 @param video_or_file string or :epkg:`VideoClip` 

769 @param pos see `set_position <https://zulko.github.io/moviepy/ref/VideoClip/ 

770 VideoClip.html?highlight=imageclip#moviepy.video.VideoClip.VideoClip.set_position>`_ 

771 @param relative see `set_position <https://zulko.github.io/moviepy/ref/VideoClip/ 

772 VideoClip.html?highlight=imageclip#moviepy.video.VideoClip.VideoClip.set_position>`_ 

773 @return :epkg:`VideoClip` 

774 

775 This function moves the video inside another one. 

776 Therefore, it has no effect if the result of this video 

777 is composed. See function @see fn video_compose. 

778 Example: 

779 

780 :: 

781 

782 from code_beatrix.art.video import video_image, video_position, video_compose, video_text 

783 

784 img = 'GastonLagaffe_1121.jpg' 

785 vidimg = video_image(img, duration=5, opacity=200) 

786 vidimg = video_position(vidimg, lambda t: (0, 0), relative=True) 

787 

788 text = video_text('boule', size=2., color=(255, 0, 0, 128), background=(0, 255, 0, 100)) 

789 text = video_position(text, lambda t: (t * 0.1, t * 0.2), relative=True) 

790 

791 comb = video_compose([vidimg, text], t1=[0, 1]) 

792 

793 You can see an example of the video it produces in notebook 

794 :ref:`video_notebook`. 

795 """ 

796 with VideoContext(video_or_file) as video: 

797 video = video.set_position(pos=pos, relative=relative) 

798 return video 

799 

800 

801def video_resize(video_or_file, newsize): 

802 """ 

803 Resizes a video. 

804 Modifie la taille d'une video. 

805 Relies on function 

806 `resize <https://zulko.github.io/moviepy/ref/videofx/moviepy.video.fx.all.resize.html#moviepy.video.fx.all.resize>`_. 

807 

808 @param video_or_file string or :epkg:`VideoClip` 

809 @param newsize `resize <https://zulko.github.io/moviepy/ref/videofx/ 

810 moviepy.video.fx.all.resize.html#moviepy.video.fx.all.resize>`_ 

811 @return :epkg:`VideoClip` 

812 """ 

813 with VideoContext(video_or_file) as video: 

814 video = video.resize(newsize) 

815 return CompositeVideoClip([video]) 

816 

817 

818def video_text(text, font=None, fontsize=32, size=None, 

819 color=None, background=None, opacity=None, 

820 **kwargs): 

821 """ 

822 Creates an image with text (:epkg:`ImageClip`). 

823 Créé une image à partir de texte. 

824 

825 @param text text 

826 @param color color 

827 @param font police name 

828 @param fontsize font size 

829 @param size image size, None to get the smallest one which 

830 contains the text, a float to get *size* times 

831 this smallest size 

832 @param background background of the image 

833 @param opacity to overwrite the opacity, 

834 *color* and *background* should be 4-uple colors, 

835 the last number in ``(0, 0, 0, 255)`` represents the 

836 opacity 

837 @param kwargs additional parameters sent to @see fn video_image 

838 @return :epkg:`ImageClip` 

839 

840 If *duration* is None, it will be fixed when the image is 

841 composed with another one. The image remains wherever it is placed. 

842 The *opacity* is a number between 0 (transparent) and 255 (opaque). 

843 0 means the image cannot be seen. The number can be set up for each 

844 pixel. By default, the image background is transparent (0). You can find 

845 many font at `google/fonts <https://github.com/google/fonts/tree/master/ofl>`_ 

846 or `msfonts <https://github.com/caarlos0-graveyard/msfonts/tree/master/fonts>`_. 

847 """ 

848 if background is None: 

849 background = (255, 255, 255, 0) 

850 if color is None: 

851 color = (0, 0, 0, 255) 

852 if isinstance(font, str): 

853 if not font.endswith('.ttf'): 

854 font += '.ttf' 

855 elif font is None: 

856 if sys.platform.startswith('win'): 

857 font = "arial.ttf" 

858 else: 

859 exp = '/usr/share/fonts/truetype/dejavu' # os.path.expanduser('~') 

860 d = exp # os.path.join(exp, '.local', 'share', 'fonts') 

861 if not os.path.exists(d): 

862 raise FileNotFoundError("Unable to find '{0}'".format(d)) 

863 font = os.path.join(d, "DejaVuSans.ttf") 

864 if not os.path.exists(font): 

865 raise FileNotFoundError("Unable to find font '{0}'. Available:\n{1}".format( 

866 font, "\n".join(os.listdir(d)))) 

867 try: 

868 obj = ImageFont.truetype(font=font, size=fontsize) 

869 except OSError as e: 

870 raise FontError("Unable to find font '{0}'".format(font)) from e 

871 if size is None: 

872 size = obj.getsize(text) 

873 elif isinstance(size, (float, int)): 

874 fs = obj.getsize(text) 

875 size = (int(fs[0] * size), int(fs[1] * size)) 

876 elif not isinstance(size, tuple): 

877 raise TypeError("size should be a tuple or a float") 

878 if opacity is not None: 

879 if len(color) == 3: 

880 color = color + (opacity,) 

881 elif len(color) == 4: 

882 color = color[:3] + (opacity,) 

883 else: 

884 raise ValueError("color should a 3 or 4 tuple") 

885 img = Image.new('RGBA', size, background) 

886 draw = ImageDraw.Draw(img) 

887 draw.text((0, 0), text, font=obj, fill=color) 

888 return video_image(img, **kwargs) 

889 

890 

891def video_frame(fct_frame, **kwargs): 

892 """ 

893 Creates a video from drawing or images. 

894 *fct_frame* can either be a function which draws a picture at time *t* 

895 or a list of picture names or a folder. 

896 Créé une vidéo à partir de dessins ou d'images. 

897 *fct_frame* est soit une fonction qui dessine chaque image à chaque instant *t*, 

898 une liste de noms d'images ou un répertoire. 

899 

900 @param fct_frame function like ``def make_frame(t: float) -> numpy.ndarray``, 

901 or list of images or folder name 

902 @param kwargs additional arguments for function 

903 `make_frame <https://zulko.github.io/moviepy/getting_started/videoclips.html#videoclip>`_ 

904 @return :epkg:`VideoClip` 

905 """ 

906 if isinstance(fct_frame, str): 

907 if not os.path.exists(fct_frame): 

908 raise FileNotFoundError( 

909 "Unable to find folder '{0}'".format(fct_frame)) 

910 imgs = os.listdir(fct_frame) 

911 exts = {'.jpg', '.jpeg', '.png', '.bmp', '.tiff'} 

912 imgs = [os.path.join(fct_frame, _) 

913 for _ in imgs if os.path.splitext(_)[-1].lower() in exts] 

914 return video_frame(imgs, **kwargs) 

915 elif isinstance(fct_frame, list): 

916 for img in fct_frame: 

917 if not os.path.exists(img): 

918 raise FileNotFoundError( 

919 "Unable to find image '{0}'".format(img)) 

920 return ImageSequenceClip(fct_frame, **kwargs) 

921 else: 

922 return VideoClip(fct_frame, **kwargs)