aurclips 0.2.0__tar.gz → 0.2.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. {aurclips-0.2.0 → aurclips-0.2.1}/PKG-INFO +1 -1
  2. {aurclips-0.2.0 → aurclips-0.2.1}/aurclips/__init__.py +1 -1
  3. {aurclips-0.2.0 → aurclips-0.2.1}/aurclips/marks.py +35 -16
  4. aurclips-0.2.1/aurclips/player.py +190 -0
  5. {aurclips-0.2.0 → aurclips-0.2.1}/docs/grabar-en-beats.md +8 -4
  6. aurclips-0.2.0/aurclips/player.py +0 -97
  7. {aurclips-0.2.0 → aurclips-0.2.1}/.gitignore +0 -0
  8. {aurclips-0.2.0 → aurclips-0.2.1}/CONTEXT.md +0 -0
  9. {aurclips-0.2.0 → aurclips-0.2.1}/LICENSE +0 -0
  10. {aurclips-0.2.0 → aurclips-0.2.1}/README.md +0 -0
  11. {aurclips-0.2.0 → aurclips-0.2.1}/aurclips/__main__.py +0 -0
  12. {aurclips-0.2.0 → aurclips-0.2.1}/aurclips/assets/config.default.yaml +0 -0
  13. {aurclips-0.2.0 → aurclips-0.2.1}/aurclips/assets/fonts/Anton-Regular.ttf +0 -0
  14. {aurclips-0.2.0 → aurclips-0.2.1}/aurclips/assets/fonts/OFL.txt +0 -0
  15. {aurclips-0.2.0 → aurclips-0.2.1}/aurclips/clipper.py +0 -0
  16. {aurclips-0.2.0 → aurclips-0.2.1}/aurclips/config.py +0 -0
  17. {aurclips-0.2.0 → aurclips-0.2.1}/aurclips/facecrop.py +0 -0
  18. {aurclips-0.2.0 → aurclips-0.2.1}/aurclips/heuristics.py +0 -0
  19. {aurclips-0.2.0 → aurclips-0.2.1}/aurclips/ingest.py +0 -0
  20. {aurclips-0.2.0 → aurclips-0.2.1}/aurclips/notify.py +0 -0
  21. {aurclips-0.2.0 → aurclips-0.2.1}/aurclips/render.py +0 -0
  22. {aurclips-0.2.0 → aurclips-0.2.1}/aurclips/runner.py +0 -0
  23. {aurclips-0.2.0 → aurclips-0.2.1}/aurclips/safety.py +0 -0
  24. {aurclips-0.2.0 → aurclips-0.2.1}/aurclips/select_clips.py +0 -0
  25. {aurclips-0.2.0 → aurclips-0.2.1}/aurclips/state.py +0 -0
  26. {aurclips-0.2.0 → aurclips-0.2.1}/aurclips/stats.py +0 -0
  27. {aurclips-0.2.0 → aurclips-0.2.1}/aurclips/subtitles.py +0 -0
  28. {aurclips-0.2.0 → aurclips-0.2.1}/aurclips/titles.py +0 -0
  29. {aurclips-0.2.0 → aurclips-0.2.1}/aurclips/transcribe.py +0 -0
  30. {aurclips-0.2.0 → aurclips-0.2.1}/aurclips/upload.py +0 -0
  31. {aurclips-0.2.0 → aurclips-0.2.1}/config.yaml +0 -0
  32. {aurclips-0.2.0 → aurclips-0.2.1}/docs/adr/0001-extremos-apretados-centro-simple.md +0 -0
  33. {aurclips-0.2.0 → aurclips-0.2.1}/docs/adr/0002-recortador-en-la-puerta-publicador-dentro.md +0 -0
  34. {aurclips-0.2.0 → aurclips-0.2.1}/docs/adr/0003-rutas-checkout-y-usuario.md +0 -0
  35. {aurclips-0.2.0 → aurclips-0.2.1}/docs/config.md +0 -0
  36. {aurclips-0.2.0 → aurclips-0.2.1}/docs/pipeline.md +0 -0
  37. {aurclips-0.2.0 → aurclips-0.2.1}/docs/selection.md +0 -0
  38. {aurclips-0.2.0 → aurclips-0.2.1}/docs/upload-youtube.md +0 -0
  39. {aurclips-0.2.0 → aurclips-0.2.1}/packaging/README.md +0 -0
  40. {aurclips-0.2.0 → aurclips-0.2.1}/packaging/crontab.example +0 -0
  41. {aurclips-0.2.0 → aurclips-0.2.1}/packaging/launchd/com.aurclips.daily.plist +0 -0
  42. {aurclips-0.2.0 → aurclips-0.2.1}/packaging/systemd/aurclips.service +0 -0
  43. {aurclips-0.2.0 → aurclips-0.2.1}/packaging/systemd/aurclips.timer +0 -0
  44. {aurclips-0.2.0 → aurclips-0.2.1}/pyproject.toml +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aurclips
3
- Version: 0.2.0
3
+ Version: 0.2.1
4
4
  Summary: De videos largos a YouTube Shorts verticales con subtítulos, 100% local
5
5
  Project-URL: Homepage, https://github.com/feliivk/aurclips
6
6
  Author: Felii
@@ -1,3 +1,3 @@
1
1
  """Bot de automatización de Shorts: descarga, recorta, edita y publica."""
2
2
 
3
- __version__ = "0.2.0"
3
+ __version__ = "0.2.1"
@@ -421,33 +421,52 @@ def review_session(video_path: str | Path) -> Path:
421
421
  print(f"Repasando: {video.name}")
422
422
  if existing:
423
423
  print(f" ({len(existing)} marca(s) preexistentes; se conservan)")
424
- print(" Enter -> marca el momento que está sonando (pausar también vale)")
425
- print(" u -> deshacer la última marca de esta sesión")
424
+ print(" Enter -> marca el momento que está sonando EN EL REPRODUCTOR")
425
+ print(" (o en este terminal); pausar también vale")
426
+ print(" u -> deshacer la última marca (también en el reproductor)")
427
+ print(" volumen en mpv: 9 baja · 0 sube · m mutea")
426
428
  print(" Ctrl+C o cerrar el reproductor -> terminar y guardar\n")
427
429
 
428
430
  player = MpvPlayer(mpv, video)
429
431
  lines = _stdin_queue()
432
+
433
+ def do_mark():
434
+ t = player.playback_time()
435
+ if t is None:
436
+ print(" no pude leer la posición (¿mpv sigue abriendo?)")
437
+ elif session.mark(t) is None:
438
+ print(f" ya hay una marca en {_mmss(t)}; no se duplica")
439
+ player.show_message(f"ya marcado ({_mmss(t)})")
440
+ else:
441
+ n = len(session.all_marks())
442
+ print(f" marca {n:>2} en {_mmss(t)}")
443
+ player.show_message(f"marca {n} - {_mmss(t)}")
444
+
445
+ def do_undo():
446
+ undone = session.undo()
447
+ if undone is None:
448
+ print(" nada que deshacer en esta sesión")
449
+ player.show_message("nada que deshacer")
450
+ else:
451
+ print(f" deshecha la marca de {_mmss(undone)}")
452
+ player.show_message(f"deshecha la marca de {_mmss(undone)}")
453
+
430
454
  try:
431
455
  while player.alive():
456
+ # dos fuentes con las mismas teclas: el reproductor y el terminal
457
+ event = player.poll_event(timeout=0.15)
458
+ if event == "mark":
459
+ do_mark()
460
+ elif event == "undo":
461
+ do_undo()
432
462
  try:
433
- line = lines.get(timeout=0.3)
463
+ command = lines.get_nowait().strip().lower()
434
464
  except queue.Empty:
435
465
  continue
436
- command = line.strip().lower()
437
466
  if command == "u":
438
- undone = session.undo()
439
- if undone is None:
440
- print(" nada que deshacer en esta sesión")
441
- else:
442
- print(f" deshecha la marca de {_mmss(undone)}")
467
+ do_undo()
443
468
  elif command == "":
444
- t = player.playback_time()
445
- if t is None:
446
- print(" no pude leer la posición (¿mpv sigue abriendo?)")
447
- elif session.mark(t) is None:
448
- print(f" ya hay una marca en {_mmss(t)}; no se duplica")
449
- else:
450
- print(f" marca {len(session.all_marks()):>2} en {_mmss(t)}")
469
+ do_mark()
451
470
  # cualquier otra entrada se ignora sin ruido
452
471
  except KeyboardInterrupt:
453
472
  print()
@@ -0,0 +1,190 @@
1
+ """Transporte con mpv para el repaso: lanzar, preguntar la posición, cerrar.
2
+
3
+ Delgado a propósito y sin tests automáticos — el mismo trato que ffmpeg: aquí
4
+ no hay lógica, solo el cable. La lógica del repaso vive en marks.MarkingSession
5
+ y se prueba sin nada de esto.
6
+
7
+ mpv es el único reproductor multiplataforma con un IPC limpio para preguntar
8
+ ``playback-time``: un named pipe en Windows, un unix socket en POSIX, JSON por
9
+ línea en ambos. Es dependencia opcional solo de este modo.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import json
15
+ import os
16
+ import queue
17
+ import shutil
18
+ import subprocess
19
+ import tempfile
20
+ import threading
21
+ import time
22
+ from pathlib import Path
23
+
24
+ from .config import ROOT, _exe_suffix
25
+
26
+ # Teclas que marcan DENTRO del reproductor. La primera prueba real lo dejó
27
+ # claro: quien repasa tiene las manos y los ojos en el video, no en el
28
+ # terminal — los Enter iban a la ventana de mpv y se perdían. Se le piden a
29
+ # mpv por IPC (comando keybind -> script-message) y llegan como eventos
30
+ # client-message; las mismas teclas que en el terminal.
31
+ KEY_BINDINGS = (
32
+ ("ENTER", "aurclips-mark"),
33
+ ("u", "aurclips-undo"),
34
+ )
35
+
36
+
37
+ def find_mpv() -> str:
38
+ """Ruta de mpv, o un error de una línea con la instalación por SO.
39
+
40
+ Como los demás binarios del repo: primero el empaquetado en tools/ (la
41
+ convención de Windows, donde también viven ffmpeg y deno), luego el PATH.
42
+ """
43
+ bundled = ROOT / "tools" / "mpv" / f"mpv{_exe_suffix()}"
44
+ if bundled.exists():
45
+ return str(bundled)
46
+ found = shutil.which("mpv")
47
+ if found:
48
+ return found
49
+ raise FileNotFoundError(
50
+ "No se encontró mpv (el reproductor del repaso). Instálalo y agrégalo "
51
+ "al PATH (macOS: brew install mpv · Linux: apt install mpv · "
52
+ "Windows: winget install mpv), o deja el portable en tools/mpv."
53
+ )
54
+
55
+
56
+ class MpvPlayer:
57
+ """Una instancia de mpv reproduciendo un video, con su socket de IPC."""
58
+
59
+ def __init__(self, mpv_path: str, video_path: str | Path):
60
+ if os.name == "nt":
61
+ self._ipc = rf"\\.\pipe\aurclips-mpv-{os.getpid()}"
62
+ else:
63
+ self._ipc = str(Path(tempfile.gettempdir()) / f"aurclips-mpv-{os.getpid()}.sock")
64
+ # stdin=DEVNULL: el terminal es de la sesión de marcado; sin esto mpv
65
+ # heredaría la consola y competiría con el hilo lector por las teclas
66
+ self._proc = subprocess.Popen(
67
+ [mpv_path, f"--input-ipc-server={self._ipc}", "--", str(video_path)],
68
+ stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL,
69
+ stderr=subprocess.DEVNULL,
70
+ )
71
+ self._events: queue.Queue[str] = queue.Queue()
72
+ threading.Thread(target=self._event_pump, daemon=True).start()
73
+
74
+ def poll_event(self, timeout: float) -> str | None:
75
+ """'mark' o 'undo' si el usuario pulsó la tecla en el reproductor."""
76
+ try:
77
+ return self._events.get(timeout=timeout)
78
+ except queue.Empty:
79
+ return None
80
+
81
+ def show_message(self, text: str) -> None:
82
+ """OSD en el reproductor: el eco de la marca, donde están los ojos."""
83
+ try:
84
+ self._roundtrip(json.dumps({"command": ["show-text", text, 1500]}) + "\n")
85
+ except OSError:
86
+ pass
87
+
88
+ def _event_pump(self) -> None:
89
+ """Hilo de eventos: registra las teclas en mpv y escucha sus pulsaciones.
90
+
91
+ Conexión propia y persistente (el resto de peticiones va por conexiones
92
+ cortas aparte, así no se entrelazan). Muere sola cuando mpv se cierra.
93
+ """
94
+ conn = self._connect_for_events()
95
+ if conn is None:
96
+ return
97
+ write, read, close = conn
98
+ try:
99
+ for key, message in KEY_BINDINGS:
100
+ write((json.dumps({"command": ["keybind", key,
101
+ f"script-message {message}"]}) + "\n").encode("utf-8"))
102
+ buffer = b""
103
+ while True:
104
+ data = read()
105
+ if not data: # mpv cerró la conexión
106
+ return
107
+ buffer += data
108
+ while b"\n" in buffer:
109
+ line, buffer = buffer.split(b"\n", 1)
110
+ try:
111
+ payload = json.loads(line)
112
+ except json.JSONDecodeError:
113
+ continue
114
+ if payload.get("event") == "client-message":
115
+ args = payload.get("args") or []
116
+ if args and str(args[0]).startswith("aurclips-"):
117
+ self._events.put(str(args[0]).removeprefix("aurclips-"))
118
+ except OSError:
119
+ return
120
+ finally:
121
+ try:
122
+ close()
123
+ except OSError:
124
+ pass
125
+
126
+ def _connect_for_events(self):
127
+ """(write, read, close) sobre el IPC, esperando a que mpv lo cree."""
128
+ for _ in range(40): # mpv tarda un instante en crear el socket/pipe
129
+ if not self.alive():
130
+ return None
131
+ try:
132
+ if os.name == "nt":
133
+ pipe = open(self._ipc, "r+b", buffering=0)
134
+ return pipe.write, (lambda: pipe.read(4096)), pipe.close
135
+ import socket
136
+ sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
137
+ sock.connect(self._ipc)
138
+ return sock.sendall, (lambda: sock.recv(4096)), sock.close
139
+ except OSError:
140
+ time.sleep(0.25)
141
+ return None
142
+
143
+ def alive(self) -> bool:
144
+ return self._proc.poll() is None
145
+
146
+ def playback_time(self) -> float | None:
147
+ """Posición de reproducción en segundos; None si no se pudo leer."""
148
+ request = json.dumps({"command": ["get_property", "playback-time"]}) + "\n"
149
+ try:
150
+ for line in self._roundtrip(request):
151
+ try:
152
+ payload = json.loads(line)
153
+ except json.JSONDecodeError:
154
+ continue
155
+ if "error" in payload: # respuesta a la petición (no un evento)
156
+ if payload["error"] == "success" and isinstance(
157
+ payload.get("data"), (int, float)):
158
+ return float(payload["data"])
159
+ return None
160
+ except OSError:
161
+ return None
162
+ return None
163
+
164
+ def _roundtrip(self, request: str) -> list[str]:
165
+ """Envía una petición y devuelve las líneas de respuesta."""
166
+ if os.name == "nt":
167
+ # el pipe lo crea mpv; abrir/escribir/leer por petición es lo robusto
168
+ with open(self._ipc, "r+b", buffering=0) as pipe:
169
+ pipe.write(request.encode("utf-8"))
170
+ return pipe.read(4096).decode("utf-8", "replace").splitlines()
171
+ import socket
172
+ with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as sock:
173
+ sock.settimeout(1.0)
174
+ sock.connect(self._ipc)
175
+ sock.sendall(request.encode("utf-8"))
176
+ chunks = b""
177
+ while b"\n" not in chunks:
178
+ data = sock.recv(4096)
179
+ if not data:
180
+ break
181
+ chunks += data
182
+ return chunks.decode("utf-8", "replace").splitlines()
183
+
184
+ def close(self) -> None:
185
+ if self.alive():
186
+ self._proc.terminate()
187
+ try:
188
+ self._proc.wait(timeout=3)
189
+ except subprocess.TimeoutExpired:
190
+ self._proc.kill()
@@ -91,13 +91,16 @@ material descargado de canales directamente no es tuyo. Para eso está el
91
91
  aurclips mark mi-grabacion.mp4
92
92
  ```
93
93
 
94
- Se abre el video en el reproductor y el terminal queda en modo sesión:
94
+ Se abre el video en el reproductor y marcas **sin salir de él**:
95
95
 
96
- - **Enter** marca el momento que está sonando. Pausar también vale: la marca
97
- cae donde dejaste el cursor, con la precisión que quieras.
96
+ - **Enter** (en el reproductor o en el terminal) marca el momento que está
97
+ sonando, con confirmación en la propia pantalla del video. Pausar también
98
+ vale: la marca cae donde dejaste el cursor, con la precisión que quieras.
98
99
  - **u** deshace la última marca de esta sesión (las que ya existían no se
99
100
  tocan).
100
101
  - **Ctrl+C** o cerrar el reproductor termina y guarda.
102
+ - Controles útiles de mpv: espacio pausa, flechas buscan, `[` y `]` cambian la
103
+ velocidad, `9`/`0` bajan/suben el volumen, `m` mutea.
101
104
 
102
105
  Las marcas van al mismo `<video>.marks.txt` de siempre, junto al video, y las
103
106
  que ya hubiera —de la voz, del hotkey o de un repaso anterior— se conservan:
@@ -116,7 +119,8 @@ aurclips clip partida.mp4 # los momentos marcados son los recortes
116
119
 
117
120
  El repaso necesita **[mpv](https://mpv.io)** (solo este modo lo usa): `winget
118
121
  install mpv` en Windows, `brew install mpv` en macOS, `apt install mpv` en
119
- Linux.
122
+ Linux. En Windows también vale dejar el [build portable](https://mpv.io/installation/)
123
+ descomprimido en `tools/mpv`, junto a los demás binarios del repo.
120
124
 
121
125
  ## Qué NO tienes que hacer
122
126
 
@@ -1,97 +0,0 @@
1
- """Transporte con mpv para el repaso: lanzar, preguntar la posición, cerrar.
2
-
3
- Delgado a propósito y sin tests automáticos — el mismo trato que ffmpeg: aquí
4
- no hay lógica, solo el cable. La lógica del repaso vive en marks.MarkingSession
5
- y se prueba sin nada de esto.
6
-
7
- mpv es el único reproductor multiplataforma con un IPC limpio para preguntar
8
- ``playback-time``: un named pipe en Windows, un unix socket en POSIX, JSON por
9
- línea en ambos. Es dependencia opcional solo de este modo.
10
- """
11
-
12
- from __future__ import annotations
13
-
14
- import json
15
- import os
16
- import shutil
17
- import subprocess
18
- import tempfile
19
- from pathlib import Path
20
-
21
-
22
- def find_mpv() -> str:
23
- """Ruta de mpv, o un error de una línea con la instalación por SO."""
24
- found = shutil.which("mpv")
25
- if found:
26
- return found
27
- raise FileNotFoundError(
28
- "No se encontró mpv (el reproductor del repaso). Instálalo y agrégalo "
29
- "al PATH (macOS: brew install mpv · Linux: apt install mpv · "
30
- "Windows: winget install mpv)."
31
- )
32
-
33
-
34
- class MpvPlayer:
35
- """Una instancia de mpv reproduciendo un video, con su socket de IPC."""
36
-
37
- def __init__(self, mpv_path: str, video_path: str | Path):
38
- if os.name == "nt":
39
- self._ipc = rf"\\.\pipe\aurclips-mpv-{os.getpid()}"
40
- else:
41
- self._ipc = str(Path(tempfile.gettempdir()) / f"aurclips-mpv-{os.getpid()}.sock")
42
- # stdin=DEVNULL: el terminal es de la sesión de marcado; sin esto mpv
43
- # heredaría la consola y competiría con el hilo lector por las teclas
44
- self._proc = subprocess.Popen(
45
- [mpv_path, f"--input-ipc-server={self._ipc}", "--", str(video_path)],
46
- stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL,
47
- stderr=subprocess.DEVNULL,
48
- )
49
-
50
- def alive(self) -> bool:
51
- return self._proc.poll() is None
52
-
53
- def playback_time(self) -> float | None:
54
- """Posición de reproducción en segundos; None si no se pudo leer."""
55
- request = json.dumps({"command": ["get_property", "playback-time"]}) + "\n"
56
- try:
57
- for line in self._roundtrip(request):
58
- try:
59
- payload = json.loads(line)
60
- except json.JSONDecodeError:
61
- continue
62
- if "error" in payload: # respuesta a la petición (no un evento)
63
- if payload["error"] == "success" and isinstance(
64
- payload.get("data"), (int, float)):
65
- return float(payload["data"])
66
- return None
67
- except OSError:
68
- return None
69
- return None
70
-
71
- def _roundtrip(self, request: str) -> list[str]:
72
- """Envía una petición y devuelve las líneas de respuesta."""
73
- if os.name == "nt":
74
- # el pipe lo crea mpv; abrir/escribir/leer por petición es lo robusto
75
- with open(self._ipc, "r+b", buffering=0) as pipe:
76
- pipe.write(request.encode("utf-8"))
77
- return pipe.read(4096).decode("utf-8", "replace").splitlines()
78
- import socket
79
- with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as sock:
80
- sock.settimeout(1.0)
81
- sock.connect(self._ipc)
82
- sock.sendall(request.encode("utf-8"))
83
- chunks = b""
84
- while b"\n" not in chunks:
85
- data = sock.recv(4096)
86
- if not data:
87
- break
88
- chunks += data
89
- return chunks.decode("utf-8", "replace").splitlines()
90
-
91
- def close(self) -> None:
92
- if self.alive():
93
- self._proc.terminate()
94
- try:
95
- self._proc.wait(timeout=3)
96
- except subprocess.TimeoutExpired:
97
- self._proc.kill()
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes