aurclips 0.1.0__py3-none-any.whl

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.
aurclips/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """Bot de automatización de Shorts: descarga, recorta, edita y publica."""
2
+
3
+ __version__ = "0.1.0"
aurclips/__main__.py ADDED
@@ -0,0 +1,366 @@
1
+ """CLI de aurclips.
2
+
3
+ Uso:
4
+ python -m aurclips clip RUTA # recortar una grabación y ya (sin pipeline)
5
+ python -m aurclips run # pipeline completo (ingesta -> proceso -> subida)
6
+ python -m aurclips mark # marcar momentos en vivo mientras grabas
7
+ python -m aurclips review # aprobar o corregir títulos antes de subir
8
+ python -m aurclips ingest # solo buscar/descargar contenido nuevo
9
+ python -m aurclips process # solo transcribir + seleccionar + renderizar
10
+ python -m aurclips upload # solo subir clips renderizados
11
+ python -m aurclips auth # iniciar sesión de YouTube (una sola vez)
12
+ python -m aurclips status # ver estado de videos y clips
13
+ python -m aurclips report # métricas de los Shorts publicados y cola
14
+ python -m aurclips retry # reencolar videos/clips fallidos
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import argparse
20
+ import json
21
+ import sys
22
+ import traceback
23
+ from pathlib import Path
24
+
25
+ from .config import Config
26
+ from .state import State
27
+
28
+
29
+ def _load() -> tuple[Config, State]:
30
+ cfg = Config()
31
+ db = State(cfg.db_path)
32
+ return cfg, db
33
+
34
+
35
+ def cmd_ingest(cfg: Config, db: State):
36
+ from .ingest import ingest
37
+ ingest(cfg, db)
38
+
39
+
40
+ def cmd_process(cfg: Config, db: State):
41
+ from .render import render_clip
42
+ from .safety import screen_clip
43
+ from .select_clips import clip_words, select_clips
44
+ from .transcribe import transcribe
45
+
46
+ videos = db.videos_to_process()
47
+ if not videos:
48
+ print("[2/4] No hay videos pendientes por procesar")
49
+ return
50
+ max_videos = cfg.get("limits.max_videos_per_run", 3)
51
+ if len(videos) > max_videos:
52
+ print(f"[2/4] {len(videos)} pendientes; se procesan {max_videos} "
53
+ f"(limits.max_videos_per_run)")
54
+ videos = videos[:max_videos]
55
+ for video in videos:
56
+ vid = video["id"]
57
+ title = video["title"] or f"video_{vid}"
58
+ workdir = cfg.work_dir / f"video_{vid}"
59
+ transcript_path = workdir / "transcript.json"
60
+ try:
61
+ # --- transcribir -------------------------------------------
62
+ if db.needs_transcription(video):
63
+ print(f"[2/4] Transcribiendo: {title}")
64
+ transcript = transcribe(cfg, video["path"], transcript_path)
65
+ db.video_transcribed(vid)
66
+ else:
67
+ transcript = json.loads(transcript_path.read_text(encoding="utf-8"))
68
+
69
+ # --- seleccionar clips -------------------------------------
70
+ if db.needs_selection(video):
71
+ print(f"[3/4] Seleccionando clips: {title}")
72
+ clips = select_clips(cfg, transcript, title, video["path"])
73
+ if not clips:
74
+ print(" sin clips útiles; video marcado como terminado")
75
+ db.video_finished(vid)
76
+ continue
77
+ added = 0
78
+ for i, c in enumerate(clips):
79
+ text = " ".join(
80
+ w["word"] for w in clip_words(transcript, c.start, c.end)
81
+ )
82
+ known = ((row["id"], row["text"])
83
+ for row in db.texts_for_dedup())
84
+ verdict = screen_clip(cfg, text, known)
85
+ if verdict.unsafe_terms:
86
+ print(f" [filtro] clip {c.title!r} contiene: "
87
+ f"{', '.join(verdict.unsafe_terms[:5])} -> "
88
+ f"{cfg.get('safety.action', 'skip')}")
89
+ if verdict.duplicate_of is not None:
90
+ print(f" [dedup] clip {c.title!r} es casi idéntico "
91
+ f"al clip #{verdict.duplicate_of}; se omite")
92
+ if not verdict.keep:
93
+ continue
94
+ db.add_clip(vid, i, c, text, flagged=verdict.flagged)
95
+ added += 1
96
+ if not added:
97
+ print(" todos los clips fueron filtrados; video terminado")
98
+ db.video_finished(vid)
99
+ continue
100
+ db.video_selected(vid)
101
+
102
+ # --- renderizar --------------------------------------------
103
+ for clip in db.clips_to_render(vid):
104
+ words = clip_words(transcript, clip["start"], clip["end"])
105
+ out = render_clip(cfg, video["path"], clip["start"], clip["end"],
106
+ clip["title"], words, clip["id"])
107
+ db.clip_rendered(clip["id"], str(out))
108
+ db.video_finished(vid)
109
+ except Exception as e: # noqa: BLE001 — un video fallido no detiene el resto
110
+ print(f" [error] video {vid} ({title}): {e}")
111
+ traceback.print_exc()
112
+ db.video_failed(vid, str(e))
113
+ from .notify import notify
114
+ notify(cfg, "error", f"Falló el video '{title}': {str(e)[:200]}")
115
+
116
+
117
+ def cmd_mark(cfg: Config, db: State, name: str | None = None):
118
+ """Sesión de marcado en vivo: cada Enter marca el instante actual."""
119
+ from .marks import record_session
120
+ record_session(cfg, name)
121
+
122
+
123
+ def _show_clip(clip, tags: list[str]):
124
+ dur = clip["end"] - clip["start"]
125
+ star = " ★ marcado por ti" if clip["marked"] else ""
126
+ print(f"\n── clip #{clip['id']} · {clip['start']:.0f}s-{clip['end']:.0f}s "
127
+ f"({dur:.0f}s){star}")
128
+ print(f" archivo: {clip['path']}")
129
+ print(f" título: {clip['title']}")
130
+ if clip["description"]:
131
+ print(f" desc: {clip['description'][:160]}")
132
+ if tags:
133
+ print(f" tags: {' '.join('#' + t for t in tags)}")
134
+
135
+
136
+ def cmd_review(cfg: Config, db: State):
137
+ """Revisa y aprueba la metadata antes de que los clips se suban.
138
+
139
+ Son unos pocos al día: la herramienta propone y tú apruebas. Es el punto
140
+ donde tu criterio entra al pipeline sin tener que tocar código.
141
+ """
142
+ from . import titles
143
+ from .stats import review_header
144
+
145
+ clips = db.clips_to_review()
146
+ if not clips:
147
+ print("No hay clips esperando revisión.")
148
+ return
149
+ llm = titles.enabled(cfg)
150
+ for line in review_header(db): # los datos, donde se toman las decisiones
151
+ print(line)
152
+ print()
153
+ print(f"{len(clips)} clip(s) por revisar.")
154
+ print("[Enter] aprobar · [t] título · [d] descripción · "
155
+ f"{'[r] regenerar · ' if llm else ''}[x] descartar · [s] saltar · [q] salir")
156
+
157
+ approved = discarded = 0
158
+ for clip in clips:
159
+ tags = db.clip_tags(clip)
160
+ title, description = clip["title"], clip["description"]
161
+ _show_clip(clip, tags)
162
+ while True:
163
+ try:
164
+ choice = input(" > ").strip().lower()
165
+ except (EOFError, KeyboardInterrupt):
166
+ print("\nRevisión interrumpida; el resto queda pendiente.")
167
+ return
168
+ if choice in ("", "a", "ok"):
169
+ db.clip_approved(clip["id"], title, description, tags)
170
+ approved += 1
171
+ break
172
+ if choice == "t":
173
+ new = input(" título: ").strip()
174
+ if new:
175
+ title = new[:93]
176
+ print(f" título: {title}")
177
+ continue
178
+ if choice == "d":
179
+ new = input(" descripción: ").strip()
180
+ if new:
181
+ description = new
182
+ continue
183
+ if choice == "r" and llm:
184
+ proposal = titles.propose(cfg, clip["text"] or description, "")
185
+ if proposal: # se guarda al aprobar, no antes
186
+ title, description, tags = proposal
187
+ print(f" título: {title}")
188
+ print(f" desc: {description[:160]}")
189
+ continue
190
+ if choice == "x":
191
+ db.clip_discarded(clip["id"])
192
+ discarded += 1
193
+ print(" descartado (no se subirá)")
194
+ break
195
+ if choice == "s":
196
+ break
197
+ if choice == "q":
198
+ print(f"\n{approved} aprobado(s), {discarded} descartado(s).")
199
+ return
200
+ print(" opciones: Enter / t / d / r / x / s / q")
201
+
202
+ print(f"\n{approved} aprobado(s), {discarded} descartado(s).")
203
+
204
+
205
+ def cmd_upload(cfg: Config, db: State):
206
+ from .upload import upload_pending
207
+ upload_pending(cfg, db)
208
+
209
+
210
+ def cmd_auth(cfg: Config, db: State):
211
+ from .upload import get_credentials
212
+ get_credentials(cfg, interactive=True)
213
+ print("Autenticación de YouTube completada.")
214
+
215
+
216
+ def cmd_status(cfg: Config, db: State):
217
+ print("== Videos ==")
218
+ rows = db.recent_videos(20)
219
+ if not rows:
220
+ print(" (ninguno)")
221
+ for r in rows:
222
+ dur = f"{r['duration']:.0f}s" if r["duration"] else "?"
223
+ print(f" #{r['id']:<4} {r['status']:<12} [{r['source']}] {r['title'] or ''} ({dur})")
224
+ print("\n== Clips ==")
225
+ rows = db.recent_clips(20)
226
+ if not rows:
227
+ print(" (ninguno)")
228
+ review_on = cfg.get("review.enabled", True)
229
+ for r in rows:
230
+ extra = ""
231
+ situation = db.clip_situation(r, review_on)
232
+ if situation == "published":
233
+ extra = f" -> https://youtu.be/{r['youtube_id']} @ {r['publish_at'] or '?'}"
234
+ elif situation == "discarded":
235
+ extra = " (descartado en revisión)"
236
+ elif situation == "awaiting_review":
237
+ extra = " (por revisar)"
238
+ star = "★" if r["marked"] else " "
239
+ print(f" #{r['id']:<4} {star} {r['status']:<10} {r['title'] or ''}{extra}")
240
+
241
+
242
+ def cmd_report(cfg: Config, db: State):
243
+ from .stats import build_report, fetch_stats
244
+ if cfg.get("stats.enabled", True):
245
+ n = fetch_stats(cfg, db)
246
+ if n:
247
+ print(f"(métricas actualizadas para {n} clip(s))\n")
248
+ print(build_report(db))
249
+
250
+
251
+ def cmd_retry(cfg: Config, db: State):
252
+ """Reencola videos y clips fallidos."""
253
+ def has_transcript(video_id: int) -> bool:
254
+ return (cfg.work_dir / f"video_{video_id}" / "transcript.json").exists()
255
+
256
+ n = db.requeue_failed(has_transcript)
257
+ print(f"{n} elemento(s) reencolados. Corre 'run' para reintentarlos.")
258
+
259
+
260
+ def cmd_run(cfg: Config, db: State):
261
+ """Corrida diaria: se protege contra solapes y deja su propio log.
262
+
263
+ El lock reemplaza el -MultipleInstances del Task Scheduler (cron/launchd no
264
+ lo dan); la captura y rotación del log vivían en run.ps1 y ahora son iguales
265
+ en los tres SO, así que el scheduler solo llama a `aurclips run`.
266
+ """
267
+ from datetime import datetime
268
+
269
+ from .runner import prune_run_logs, single_instance, tee_output
270
+
271
+ logs_dir = cfg.logs_dir
272
+ with single_instance(logs_dir / "run.lock") as acquired:
273
+ if not acquired:
274
+ print("Ya hay una corrida en marcha; esta se omite.")
275
+ return
276
+ stamp = datetime.now().strftime("%Y-%m-%d_%H%M")
277
+ with tee_output(logs_dir / f"run_{stamp}.log"):
278
+ _run_pipeline(cfg, db)
279
+ prune_run_logs(logs_dir, keep=30)
280
+
281
+
282
+ def _run_pipeline(cfg: Config, db: State):
283
+ from .notify import notify
284
+ cmd_ingest(cfg, db)
285
+ cmd_process(cfg, db)
286
+ cmd_upload(cfg, db)
287
+ uploaded = db.count_published()
288
+ queued = db.count_queued()
289
+ notify(cfg, "run",
290
+ f"Corrida completa: {uploaded} clips subidos en total, {queued} en cola")
291
+ print("\nCorrida completa.")
292
+ # la corrida diaria pasa de madrugada: si algo espera tu criterio, que te
293
+ # busque a ti — el loop no se cierra solo
294
+ pending = len(db.clips_to_review())
295
+ if pending and cfg.get("review.enabled", True):
296
+ print(f"{pending} clip(s) esperan tu visto bueno: python -m aurclips review")
297
+ notify(cfg, "review",
298
+ f"{pending} clip(s) listos y esperando tu revisión "
299
+ f"(python -m aurclips review)")
300
+
301
+
302
+ def cmd_clip(cfg: Config, path: str | None, out: str | None,
303
+ max_clips: int | None):
304
+ """Modo recortador: una grabación entra, salen recortes sueltos.
305
+
306
+ El único comando que no abre la base: no hay progreso ni criterio que
307
+ guardar. Por eso recibe la config y no el par (cfg, db).
308
+ """
309
+ from .clipper import clip_recording
310
+
311
+ if not path:
312
+ print("Falta la grabación: aurclips clip RUTA_DEL_VIDEO")
313
+ sys.exit(2)
314
+ if max_clips is not None and max_clips < 1:
315
+ print("--clips es un tope: tiene que ser 1 o más")
316
+ sys.exit(2)
317
+ try:
318
+ clip_recording(cfg, path, out, max_clips)
319
+ except (OSError, ValueError) as e:
320
+ print(f"[error] {e}")
321
+ sys.exit(1)
322
+
323
+
324
+ COMMANDS = {
325
+ "run": cmd_run,
326
+ "mark": cmd_mark,
327
+ "review": cmd_review,
328
+ "ingest": cmd_ingest,
329
+ "process": cmd_process,
330
+ "upload": cmd_upload,
331
+ "auth": cmd_auth,
332
+ "status": cmd_status,
333
+ "report": cmd_report,
334
+ "retry": cmd_retry,
335
+ }
336
+
337
+
338
+ def main():
339
+ parser = argparse.ArgumentParser(prog="aurclips", description=__doc__,
340
+ formatter_class=argparse.RawDescriptionHelpFormatter)
341
+ parser.add_argument("command", choices=["clip", *COMMANDS])
342
+ parser.add_argument("name", nargs="?",
343
+ help="ruta de la grabación (para 'clip') o nombre de "
344
+ "la sesión (para 'mark')")
345
+ parser.add_argument("--out", metavar="CARPETA",
346
+ help="dónde dejar los recortes (solo 'clip')")
347
+ parser.add_argument("--clips", type=int, metavar="N",
348
+ help="tope de recortes en esta corrida (solo 'clip')")
349
+ args = parser.parse_args()
350
+ try:
351
+ # 'clip' no toca la base: se carga solo la config
352
+ if args.command == "clip":
353
+ cmd_clip(Config(), args.name, args.out, args.clips)
354
+ return
355
+ cfg, db = _load()
356
+ if args.command == "mark":
357
+ cmd_mark(cfg, db, args.name)
358
+ else:
359
+ COMMANDS[args.command](cfg, db)
360
+ except KeyboardInterrupt:
361
+ print("\nInterrumpido.")
362
+ sys.exit(130)
363
+
364
+
365
+ if __name__ == "__main__":
366
+ main()
@@ -0,0 +1,204 @@
1
+ # ============================================================
2
+ # Configuración de aurclips
3
+ #
4
+ # El proyecto se afina en tres puntos del flujo, no parámetro a parámetro:
5
+ # 1. ARRIBA — cómo grabas (marks): grabar en beats y marcarlos le gana a
6
+ # cualquier heurística. Es la palanca más grande.
7
+ # 2. MEDIO — el selector (selection.profile): mismo motor, calibrado a
8
+ # cómo suenas tú.
9
+ # 3. ABAJO — título (channel/titles) y medición (`report`): ahí es donde
10
+ # el canal mejora con el tiempo.
11
+ # ============================================================
12
+
13
+ # ------------------------------------------------------------
14
+ # 1) ARRIBA — tus marcas al grabar
15
+ # Di la frase en voz alta mientras grabas, o deja un
16
+ # <video>.marks.txt al lado (lo escribe `aurclips mark`).
17
+ # ------------------------------------------------------------
18
+ marks:
19
+ enabled: true
20
+ phrases: # frases gatillo: dilas y ese momento queda marcado.
21
+ - "esto es un short" # ¿un fraseo tuyo no marcó? AGRÉGALO AQUÍ; es más
22
+ - "esto va a ser un short" # seguro que bajar 'similarity': una variante
23
+ - "esto va al short" # nueva coincide al 100% sin acercar el umbral a
24
+ - "este es el short" # los falsos positivos
25
+ - "va para short"
26
+ - "marca aquí"
27
+ - "clip esto"
28
+ similarity: 0.85 # se comparan por parecido, no al pie de la letra: si un
29
+ # día dices "esto es short" o Whisper escribe "shot", la
30
+ # marca no se pierde (1.0 = exigir la frase literal).
31
+ # Negar nunca marca: "esto NO es un short" se parece un
32
+ # 91% y aun así se descarta
33
+ exclusive: true # si el video trae marcas, solo se miran esas ventanas
34
+ # (false = el resto del video sigue compitiendo).
35
+ # OJO: un video SIN marcas no se ve afectado — se
36
+ # selecciona normal, no se queda sin Shorts
37
+ tolerance: 3.0 # holgura (seg) al emparejar una marca con una ventana
38
+
39
+ # ------------------------------------------------------------
40
+ # 3) ABAJO — tu canal: contexto para escribir los títulos
41
+ # ------------------------------------------------------------
42
+ channel:
43
+ angle: "" # de qué va tu canal, en una línea
44
+ # ej: "análisis tranquilo de videojuegos, sin gritos"
45
+ title_examples: [] # 3-5 títulos tuyos que te gusten: marcan la línea
46
+ # editorial que el modelo debe imitar
47
+ # - "Por qué nadie termina este juego"
48
+ # - "El detalle que todos se saltan"
49
+
50
+ # Canales de YouTube a vigilar (URL del canal o de su pestaña /videos)
51
+ channels:
52
+ # - "https://www.youtube.com/@MiCanal/videos"
53
+
54
+ # Cuántos videos recientes revisar por canal en cada corrida
55
+ channel_scan_limit: 5
56
+
57
+ # Duración mínima (segundos) para descargar un video de un canal
58
+ # (solo aplica a canales; la carpeta inbox acepta clips de cualquier duración)
59
+ min_source_duration: 300
60
+
61
+ # Calidad máxima de descarga
62
+ max_download_height: 1080
63
+
64
+ # ------------------------------------------------------------
65
+ # Transcripción (faster-whisper, corre local y gratis)
66
+ # ------------------------------------------------------------
67
+ whisper:
68
+ model: "medium" # tiny / base / small / medium / large-v3 (más grande = mejor)
69
+ # con GPU NVIDIA, "medium" o "large-v3" van rápido y transcriben mejor
70
+ language: null # null = autodetectar; o "es", "en", etc.
71
+ device: "auto" # auto = GPU si hay, si no CPU (con respaldo automático)
72
+ compute_type: "auto" # auto elige lo mejor según el dispositivo
73
+
74
+ # ------------------------------------------------------------
75
+ # 2) MEDIO — selección de clips (100% local, sin API keys)
76
+ # ------------------------------------------------------------
77
+ selection:
78
+ profile: "comentario" # calibración del selector a TU género:
79
+ # comentario = charla tranquila (análisis, podcast,
80
+ # tutorial): el volumen dice poco, manda
81
+ # cerrar la idea y la densidad
82
+ # gaming = reacciones y streams: los picos de
83
+ # audio sí señalan el momento
84
+ weights: {} # ajuste fino sobre el perfil; cada número es lo máximo
85
+ # que esa señal mueve la puntuación. Ej:
86
+ # energy: 0.08
87
+ # closes: 0.32
88
+ # señales: energy, pace, hook, punct, closes, density,
89
+ # filler, gaps, mark
90
+ clips_per_video: 3 # tope máximo de Shorts por video largo
91
+ minutes_per_short: 4 # densidad: ~1 Short por cada N minutos de video
92
+ # (el número real = duración/N, acotado por el tope;
93
+ # 0 = desactivado: usa siempre el tope)
94
+ quality_floor: 0.55 # descarta candidatos bajo esta fracción de la
95
+ # puntuación del mejor (0 = desactivado); mejor
96
+ # pocos Shorts buenos que rellenar el cupo
97
+ # (lo que marcaste tú queda exento)
98
+ #
99
+ # ESTE ES EL DIAL DEL VOLUMEN. Es agresivo: en una
100
+ # medición sintética dejó pasar 1 de 11 candidatos, o
101
+ # sea ~1 Short por video sin marcar. Si con corridas
102
+ # reales te rinde muy poco, BAJA ESTE NÚMERO (0.40),
103
+ # no toques los pesos: los pesos deciden CUÁL clip,
104
+ # este decide CUÁNTOS.
105
+ min_clip_seconds: 15
106
+ max_clip_seconds: 59
107
+
108
+ # ------------------------------------------------------------
109
+ # 3) ABAJO — títulos escritos por un LLM local y revisados por ti
110
+ # ------------------------------------------------------------
111
+ titles:
112
+ engine: "auto" # heuristic = título sacado del propio clip (sin LLM)
113
+ # ollama = siempre pedírselo a Ollama
114
+ # auto = usa Ollama si está corriendo
115
+ model: "qwen2.5:7b" # modelo de Ollama que redacta (alternativa ligera:
116
+ # "gemma3:4b"). Instálalo con: ollama pull qwen2.5:7b
117
+ url: "http://localhost:11434"
118
+
119
+ review:
120
+ enabled: true # nada se sube sin tu visto bueno: `aurclips review`
121
+ # muestra cada clip para aprobar, corregir o descartar
122
+ # (false = pipeline 100% desatendido)
123
+
124
+ # ------------------------------------------------------------
125
+ # Edición / render
126
+ # ------------------------------------------------------------
127
+ render:
128
+ subtitles: true # subtítulos virales quemados (estilo Hormozi)
129
+ words_per_caption: 3 # palabras por frase en pantalla (aparecen una a una)
130
+ font: "Anton" # viene con el paquete; si faltara, una familia genérica
131
+ font_size: 112 # sobre lienzo de 1080x1920 (rango viral: 80-120)
132
+ outline: 10 # grosor del contorno negro
133
+ base_color: "#FFFFFF"
134
+ highlight_colors: ["#FFD93D", "#39FF14"] # palabra clave (rota amarillo/verde)
135
+ caption_position: 0.70 # altura del texto (0.70 = tercio bajo, libre de UI)
136
+ tighten_silences: true # jump cuts: recorta pausas muertas
137
+ max_pause: 1.5 # pausas más largas que esto (seg) se recortan
138
+ # (1.0 para charla pura; 1.5-2.0 en gaming conserva acción visual)
139
+ crf: 20
140
+ preset: "veryfast"
141
+
142
+ # ------------------------------------------------------------
143
+ # Encuadre inteligente
144
+ # ------------------------------------------------------------
145
+ crop:
146
+ face_tracking: true # detecta el rostro y centra el recorte vertical en él
147
+
148
+ # ------------------------------------------------------------
149
+ # Filtro de contenido y duplicados
150
+ # ------------------------------------------------------------
151
+ safety:
152
+ enabled: true
153
+ action: "skip" # skip = descartar el clip | flag = guardarlo marcado para revisión
154
+ strict: false # false = permite groserías comunes (monetizables, normales en gaming)
155
+ # true = también filtra groserías (canales de marca/infantiles)
156
+ extra_words: [] # palabras adicionales a bloquear (además de la lista integrada)
157
+
158
+ dedup:
159
+ enabled: true
160
+ similarity: 0.8 # umbral de similitud (0-1) para considerar un clip duplicado
161
+
162
+ # ------------------------------------------------------------
163
+ # Límites por corrida (control de carga y escalado)
164
+ # ------------------------------------------------------------
165
+ limits:
166
+ max_videos_per_run: 3 # videos largos procesados por corrida
167
+ max_uploads_per_run: 5 # subidas por corrida (la cuota de YouTube da ~6/día)
168
+
169
+ # ------------------------------------------------------------
170
+ # Métricas y alertas
171
+ # ------------------------------------------------------------
172
+ stats:
173
+ enabled: true # consulta vistas/likes de los Shorts publicados
174
+
175
+ alerts:
176
+ discord_webhook: null # URL de webhook de Discord para avisos (opcional)
177
+ notify_on: ["error", "uploaded", "review"] # eventos que generan aviso
178
+ # review = hay clips esperando tu aprobación; sin
179
+ # este aviso la corrida de madrugada se queda muda
180
+
181
+ # ------------------------------------------------------------
182
+ # Publicación en YouTube
183
+ # ------------------------------------------------------------
184
+ upload:
185
+ enabled: false # MODO BETA: cambia a true cuando quieras publicar de verdad
186
+ privacy: "private" # se sube en privado y YouTube lo publica solo en publish_time
187
+ publish_time: "19:00" # hora local de publicación diaria
188
+ timezone: null # null = zona horaria del sistema; o "America/Mexico_City", etc.
189
+ min_lead_hours: 1 # margen mínimo entre subir y publicar
190
+ category_id: "24" # 24 = Entertainment, 20 = Gaming, 22 = People & Blogs
191
+ extra_tags: ["shorts"]
192
+
193
+ # ------------------------------------------------------------
194
+ # Rutas (relativas a la raíz del proyecto)
195
+ # ------------------------------------------------------------
196
+ paths:
197
+ data: "data"
198
+ inbox: "data/inbox" # deja aquí videos locales para procesar
199
+ downloads: "data/downloads"
200
+ work: "data/work"
201
+ output: "data/output"
202
+ credentials: "credentials" # client_secrets.json y token de YouTube
203
+ ffmpeg: "tools/ffmpeg/bin" # ffmpeg/ffprobe empaquetados (.exe en Windows);
204
+ # si están en el PATH, se usan esos
Binary file
@@ -0,0 +1,93 @@
1
+ Copyright 2020 The Anton Project Authors (https://github.com/googlefonts/AntonFont.git)
2
+
3
+ This Font Software is licensed under the SIL Open Font License, Version 1.1.
4
+ This license is copied below, and is also available with a FAQ at:
5
+ http://scripts.sil.org/OFL
6
+
7
+
8
+ -----------------------------------------------------------
9
+ SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
10
+ -----------------------------------------------------------
11
+
12
+ PREAMBLE
13
+ The goals of the Open Font License (OFL) are to stimulate worldwide
14
+ development of collaborative font projects, to support the font creation
15
+ efforts of academic and linguistic communities, and to provide a free and
16
+ open framework in which fonts may be shared and improved in partnership
17
+ with others.
18
+
19
+ The OFL allows the licensed fonts to be used, studied, modified and
20
+ redistributed freely as long as they are not sold by themselves. The
21
+ fonts, including any derivative works, can be bundled, embedded,
22
+ redistributed and/or sold with any software provided that any reserved
23
+ names are not used by derivative works. The fonts and derivatives,
24
+ however, cannot be released under any other type of license. The
25
+ requirement for fonts to remain under this license does not apply
26
+ to any document created using the fonts or their derivatives.
27
+
28
+ DEFINITIONS
29
+ "Font Software" refers to the set of files released by the Copyright
30
+ Holder(s) under this license and clearly marked as such. This may
31
+ include source files, build scripts and documentation.
32
+
33
+ "Reserved Font Name" refers to any names specified as such after the
34
+ copyright statement(s).
35
+
36
+ "Original Version" refers to the collection of Font Software components as
37
+ distributed by the Copyright Holder(s).
38
+
39
+ "Modified Version" refers to any derivative made by adding to, deleting,
40
+ or substituting -- in part or in whole -- any of the components of the
41
+ Original Version, by changing formats or by porting the Font Software to a
42
+ new environment.
43
+
44
+ "Author" refers to any designer, engineer, programmer, technical
45
+ writer or other person who contributed to the Font Software.
46
+
47
+ PERMISSION & CONDITIONS
48
+ Permission is hereby granted, free of charge, to any person obtaining
49
+ a copy of the Font Software, to use, study, copy, merge, embed, modify,
50
+ redistribute, and sell modified and unmodified copies of the Font
51
+ Software, subject to the following conditions:
52
+
53
+ 1) Neither the Font Software nor any of its individual components,
54
+ in Original or Modified Versions, may be sold by itself.
55
+
56
+ 2) Original or Modified Versions of the Font Software may be bundled,
57
+ redistributed and/or sold with any software, provided that each copy
58
+ contains the above copyright notice and this license. These can be
59
+ included either as stand-alone text files, human-readable headers or
60
+ in the appropriate machine-readable metadata fields within text or
61
+ binary files as long as those fields can be easily viewed by the user.
62
+
63
+ 3) No Modified Version of the Font Software may use the Reserved Font
64
+ Name(s) unless explicit written permission is granted by the corresponding
65
+ Copyright Holder. This restriction only applies to the primary font name as
66
+ presented to the users.
67
+
68
+ 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
69
+ Software shall not be used to promote, endorse or advertise any
70
+ Modified Version, except to acknowledge the contribution(s) of the
71
+ Copyright Holder(s) and the Author(s) or with their explicit written
72
+ permission.
73
+
74
+ 5) The Font Software, modified or unmodified, in part or in whole,
75
+ must be distributed entirely under this license, and must not be
76
+ distributed under any other license. The requirement for fonts to
77
+ remain under this license does not apply to any document created
78
+ using the Font Software.
79
+
80
+ TERMINATION
81
+ This license becomes null and void if any of the above conditions are
82
+ not met.
83
+
84
+ DISCLAIMER
85
+ THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
86
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
87
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
88
+ OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
89
+ COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
90
+ INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
91
+ DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
92
+ FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
93
+ OTHER DEALINGS IN THE FONT SOFTWARE.