primocode 10.15.2 → 10.17.0
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.
- package/LICENSES/edit-ia-pro.txt +32 -0
- package/LICENSES/fontes-OFL.txt +476 -0
- package/LICENSES/hyperframes-Apache-2.0.txt +205 -0
- package/LICENSES/permanent-marker-Apache-2.0.txt +204 -0
- package/LICENSES/selfie-multiclass-Apache-2.0.txt +227 -0
- package/LICENSES/three-MIT.txt +23 -0
- package/LICENSES/video-use-MIT.txt +24 -0
- package/LICENSES/woven-sfx-CC0.txt +16 -0
- package/LICENSES/yunet-MIT.txt +21 -0
- package/README.md +2 -2
- package/bin/primocode.js +74 -9
- package/editor/LICENSE.md +11 -0
- package/editor/README.md +321 -0
- package/editor/SKILL.md +307 -0
- package/editor/THIRD_PARTY_NOTICES.md +228 -0
- package/editor/assets/baixar.json +591 -0
- package/editor/assets/fonts/anton/OFL.txt +93 -0
- package/editor/assets/fonts/caveat/OFL.txt +93 -0
- package/editor/assets/fonts/figtree/OFL.txt +93 -0
- package/editor/assets/fonts/fonts.json +10 -0
- package/editor/assets/fonts/montserrat/OFL.txt +93 -0
- package/editor/assets/fonts/permanentmarker/LICENSE.txt +202 -0
- package/editor/assets/models/SELFIE_MULTICLASS_LICENSE.txt +227 -0
- package/editor/assets/models/YUNET_LICENSE.txt +21 -0
- package/editor/assets/sfx/licencas/bass_drop.txt +32 -0
- package/editor/assets/sfx/licencas/cash.txt +76 -0
- package/editor/assets/sfx/licencas/charge.txt +31 -0
- package/editor/assets/sfx/licencas/click.txt +46 -0
- package/editor/assets/sfx/licencas/coin.txt +50 -0
- package/editor/assets/sfx/licencas/count.txt +47 -0
- package/editor/assets/sfx/licencas/data.txt +31 -0
- package/editor/assets/sfx/licencas/error.txt +32 -0
- package/editor/assets/sfx/licencas/flash.txt +31 -0
- package/editor/assets/sfx/licencas/gears.txt +82 -0
- package/editor/assets/sfx/licencas/keyboard.txt +48 -0
- package/editor/assets/sfx/licencas/metal_hit.txt +62 -0
- package/editor/assets/sfx/licencas/metal_riser.txt +61 -0
- package/editor/assets/sfx/licencas/mouse_click.txt +50 -0
- package/editor/assets/sfx/licencas/notification.txt +54 -0
- package/editor/assets/sfx/licencas/pop.txt +38 -0
- package/editor/assets/sfx/licencas/rewind.txt +32 -0
- package/editor/assets/sfx/licencas/rise.txt +46 -0
- package/editor/assets/sfx/licencas/sinteticos.txt +21 -0
- package/editor/assets/sfx/licencas/snap.txt +50 -0
- package/editor/assets/sfx/licencas/success.txt +40 -0
- package/editor/assets/sfx/licencas/synth_hit.txt +32 -0
- package/editor/assets/sfx/licencas/tick.txt +14 -0
- package/editor/assets/sfx/licencas/whoosh.txt +47 -0
- package/editor/assets/sfx/licencas/wind.txt +31 -0
- package/editor/assets/sfx/sfx.json +832 -0
- package/editor/assets/sfx/sfx_sinteticos.json +317 -0
- package/editor/assets/sfx/sfx_woven.json +387 -0
- package/editor/assets/sfx/woven/LICENCA.md +16 -0
- package/editor/fichas/briefing-do-video.md +45 -0
- package/editor/fichas/conferencia-da-amostra.md +56 -0
- package/editor/fichas/matriz-de-ganchos.csv +3 -0
- package/editor/fichas/padrao.json +16 -0
- package/editor/fichas/planilha-de-cenas.csv +4 -0
- package/editor/fichas/planilha-de-versoes.csv +4 -0
- package/editor/fichas/plano-em-blocos.md +32 -0
- package/editor/fichas/registro-de-materiais.json +27 -0
- package/editor/helpers/assets_ocr/ocr_vision.swift +41 -0
- package/editor/helpers/bordas_precisas.py +272 -0
- package/editor/helpers/camera_viva.py +426 -0
- package/editor/helpers/captions.py +601 -0
- package/editor/helpers/common.py +308 -0
- package/editor/helpers/compose_final.py +3555 -0
- package/editor/helpers/conferir_corte.py +234 -0
- package/editor/helpers/conferir_dados.py +405 -0
- package/editor/helpers/contact_sheet.py +282 -0
- package/editor/helpers/corte_por_onda.py +433 -0
- package/editor/helpers/corte_seco.py +595 -0
- package/editor/helpers/denoise.py +971 -0
- package/editor/helpers/efeitos.py +408 -0
- package/editor/helpers/efeitos_gerar.py +159 -0
- package/editor/helpers/elevenlabs_api.py +442 -0
- package/editor/helpers/face_layout.py +570 -0
- package/editor/helpers/fetch_images.py +423 -0
- package/editor/helpers/ganho_por_trecho.py +214 -0
- package/editor/helpers/gen_image.py +312 -0
- package/editor/helpers/gen_music.py +316 -0
- package/editor/helpers/gen_sfx.py +148 -0
- package/editor/helpers/grade.py +307 -0
- package/editor/helpers/icloud.py +119 -0
- package/editor/helpers/make_sfx.py +940 -0
- package/editor/helpers/matte.py +1342 -0
- package/editor/helpers/medir_referencia.py +2232 -0
- package/editor/helpers/mixar_trilha.py +354 -0
- package/editor/helpers/montar_corte.py +295 -0
- package/editor/helpers/motion/__init__.py +13 -0
- package/editor/helpers/motion/engine.py +1915 -0
- package/editor/helpers/motion/registry.py +510 -0
- package/editor/helpers/motion/render.py +665 -0
- package/editor/helpers/motion/templates/barra_progresso.py +370 -0
- package/editor/helpers/motion/templates/carimbo.py +132 -0
- package/editor/helpers/motion/templates/checklist.py +271 -0
- package/editor/helpers/motion/templates/comentario_cta.py +380 -0
- package/editor/helpers/motion/templates/contador.py +222 -0
- package/editor/helpers/motion/templates/destaque.py +263 -0
- package/editor/helpers/motion/templates/etiqueta_preco.py +461 -0
- package/editor/helpers/motion/templates/grafico_barras.py +510 -0
- package/editor/helpers/motion/templates/grafico_linha.py +462 -0
- package/editor/helpers/motion/templates/notificacao.py +266 -0
- package/editor/helpers/motion/templates/palavra_impacto.py +410 -0
- package/editor/helpers/motion/templates/passos.py +321 -0
- package/editor/helpers/motion/templates/riscado.py +298 -0
- package/editor/helpers/motion/templates/selo_garantia.py +150 -0
- package/editor/helpers/motion_render.py +241 -0
- package/editor/helpers/motion_sugerir.py +961 -0
- package/editor/helpers/noise_check.py +134 -0
- package/editor/helpers/pack_transcripts.py +223 -0
- package/editor/helpers/palavras_na_onda.py +176 -0
- package/editor/helpers/plano_sfx.py +839 -0
- package/editor/helpers/portao.py +427 -0
- package/editor/helpers/preview_server.py +1177 -0
- package/editor/helpers/rastrear_rosto.py +298 -0
- package/editor/helpers/render_cut.py +1256 -0
- package/editor/helpers/respiros.py +89 -0
- package/editor/helpers/sincronizar_cameras.py +571 -0
- package/editor/helpers/speech_regions.py +423 -0
- package/editor/helpers/style_catalog.json +231 -0
- package/editor/helpers/style_samples.py +246 -0
- package/editor/helpers/tela_verde.py +2113 -0
- package/editor/helpers/text_render.py +2059 -0
- package/editor/helpers/texto_na_tela.py +366 -0
- package/editor/helpers/timeline_view.py +292 -0
- package/editor/helpers/tira_de_quadros.py +113 -0
- package/editor/helpers/transcribe.py +734 -0
- package/editor/helpers/transcribe_batch.py +129 -0
- package/editor/helpers/trilhas.py +1146 -0
- package/editor/helpers/trilhas_humores.json +66 -0
- package/editor/helpers/valores.py +187 -0
- package/editor/helpers/verify_cut.py +820 -0
- package/editor/helpers/voice_levels.py +233 -0
- package/editor/helpers/voz_check.py +386 -0
- package/editor/helpers/voz_limpa.py +81 -0
- package/editor/helpers/watch_preview.py +372 -0
- package/editor/modelo-projeto/LEIA.md +402 -0
- package/editor/modelo-projeto/assets/fontes/OFL-Caveat.txt +93 -0
- package/editor/modelo-projeto/assets/fontes/OFL-JetBrainsMono.txt +93 -0
- package/editor/modelo-projeto/assets/fontes/OFL-Montserrat.txt +93 -0
- package/editor/modelo-projeto/assets/fontes/larguras.json +1 -0
- package/editor/modelo-projeto/assets/three/BufferGeometryUtils.js +1438 -0
- package/editor/modelo-projeto/assets/three/GLTFLoader.js +4895 -0
- package/editor/modelo-projeto/assets/three/LICENSE +21 -0
- package/editor/modelo-projeto/assets/three/ORIGEM.txt +17 -0
- package/editor/modelo-projeto/assets/three/RoomEnvironment.js +185 -0
- package/editor/modelo-projeto/assets/three/three.core.min.js +6 -0
- package/editor/modelo-projeto/assets/three/three.module.min.js +6 -0
- package/editor/modelo-projeto/build.py +4828 -0
- package/editor/modelo-projeto/compositions/cenas/2d/k2d_antes_depois.html +199 -0
- package/editor/modelo-projeto/compositions/cenas/2d/k2d_arquivo.html +232 -0
- package/editor/modelo-projeto/compositions/cenas/2d/k2d_busca.html +315 -0
- package/editor/modelo-projeto/compositions/cenas/2d/k2d_comando.html +304 -0
- package/editor/modelo-projeto/compositions/cenas/2d/k2d_conversa.html +217 -0
- package/editor/modelo-projeto/compositions/cenas/2d/k2d_esquema.html +289 -0
- package/editor/modelo-projeto/compositions/cenas/2d/k2d_medidor.html +249 -0
- package/editor/modelo-projeto/compositions/cenas/2d/k2d_notas.html +233 -0
- package/editor/modelo-projeto/compositions/cenas/3d/k3d_caminho.html +197 -0
- package/editor/modelo-projeto/compositions/cenas/3d/k3d_explodida.html +152 -0
- package/editor/modelo-projeto/compositions/cenas/3d/k3d_tela.html +167 -0
- package/editor/modelo-projeto/compositions/cenas/3d/k3d_volume.html +124 -0
- package/editor/modelo-projeto/compositions/cenas/3d/motor-three.js +728 -0
- package/editor/modelo-projeto/compositions/cenas/LEIA.md +177 -0
- package/editor/modelo-projeto/ferramentas/medir_fontes.py +65 -0
- package/editor/modelo-projeto/ferramentas/pessoa_recortada.py +176 -0
- package/editor/modelo-projeto/ferramentas/rastrear_maos.py +189 -0
- package/editor/modelo-projeto/hyperframes.json +12 -0
- package/editor/modelo-projeto/package.json +11 -0
- package/editor/modelo-projeto/projeto.json +16 -0
- package/editor/preview/app.css +1009 -0
- package/editor/preview/app.js +2122 -0
- package/editor/preview/index.html +307 -0
- package/editor/pyproject.toml +23 -0
- package/editor/references/3d.md +236 -0
- package/editor/references/animacao-catalogo.md +672 -0
- package/editor/references/animacao.md +498 -0
- package/editor/references/comandos.md +92 -0
- package/editor/references/corte-seco-como-funciona.md +50 -0
- package/editor/references/corte-seco.md +88 -0
- package/editor/references/corte.md +290 -0
- package/editor/references/direcao-por-blocos.md +218 -0
- package/editor/references/enquadramento.md +654 -0
- package/editor/references/estilos-prontos.md +355 -0
- package/editor/references/estudio.md +100 -0
- package/editor/references/exemplo-eventos.json +67 -0
- package/editor/references/lettering.md +252 -0
- package/editor/references/modelos.md +494 -0
- package/editor/references/phase2.md +1007 -0
- package/editor/references/referencia-aplicar.md +203 -0
- package/editor/references/referencia-medir.md +375 -0
- package/editor/references/revisao.md +335 -0
- package/editor/references/som-e-trilha.md +146 -0
- package/editor/references/tabela-de-sons.md +130 -0
- package/editor/references/variacoes.md +234 -0
- package/editor/uv.lock +1141 -0
- package/lib/act.js +38 -8
- package/lib/app-chat.js +97 -0
- package/lib/catalogo.js +7 -1
- package/lib/chamada-torta.js +81 -0
- package/lib/editor-pro.js +786 -0
- package/lib/entrega.js +58 -0
- package/lib/estudio/conferencia-visual.js +76 -0
- package/lib/estudio/diretor.js +6 -1
- package/lib/estudio/exportar.js +3 -1
- package/lib/estudio/gosto/tela.html +1349 -0
- package/lib/estudio/gosto.js +305 -0
- package/lib/estudio/index.js +12 -1
- package/lib/estudio/kit-ae/base.jsx +119 -13
- package/lib/estudio/kit-ae/cenas.jsx +47 -15
- package/lib/estudio/kit-ae/guarda.jsx +274 -0
- package/lib/estudio/kit-ae/primitivas.jsx +180 -24
- package/lib/estudio/maquina-diretor.js +70 -12
- package/lib/estudio/maquina-foto.js +177 -0
- package/lib/estudio/maquina-guarda.js +207 -0
- package/lib/estudio/maquina-layout.js +22 -1
- package/lib/estudio/maquina.js +64 -11
- package/lib/estudio/referencias.js +1 -1
- package/lib/estudio/visao/visao.swift +97 -0
- package/lib/estudio/visao.js +60 -0
- package/lib/headless.js +15 -3
- package/lib/local/cerebro.json +285 -7
- package/lib/local/motor.js +5 -2
- package/lib/memoria-usuario.js +164 -0
- package/lib/nuvem.js +33 -2
- package/lib/passagem.js +50 -0
- package/lib/perigo.js +19 -11
- package/lib/protecao.js +134 -0
- package/lib/repeticao.js +1 -1
- package/lib/sandbox.js +57 -9
- package/lib/titulo-conversa.js +46 -0
- package/lib/tools.js +23 -4
- package/lib/ui.js +2 -2
- package/package.json +3 -2
- package/skills/editar-meu-video/SKILL.md +6 -2
- package/skills/editar-video/SKILL.md +113 -0
- package/studio/pessoa.py +52 -0
- package/voz/primo_voz/__main__.py +55 -40
- package/voz/primo_voz/app.py +500 -4
- package/voz/primo_voz/chat.html +1773 -0
- package/voz/primo_voz/conversa.py +0 -26
- package/voz/primo_voz/hud.html +8 -2
- package/voz/primo_voz/hud.py +129 -5
- package/voz/primo_voz/identidade.py +1 -1
- package/voz/primo_voz/painel_dados.py +134 -0
|
@@ -6,7 +6,6 @@ teste mede. O que toca hardware (escuta.py, hud.py, __main__.py) só chama isto.
|
|
|
6
6
|
Maquina ouvindo → pensando → falando → (interrompido) → ouvindo
|
|
7
7
|
Fatiador texto em streaming → frases prontas para falar
|
|
8
8
|
resumo resposta longa → (fala curta, o que vai para o painel)
|
|
9
|
-
ativacao "Primo, abre o projeto" → (True, "abre o projeto")
|
|
10
9
|
comando "esconde o painel" → "esconder"
|
|
11
10
|
confirmacao ferramenta → "rodando os testes"
|
|
12
11
|
"""
|
|
@@ -165,31 +164,6 @@ def resumo(texto: str, frases: int = 60, teto: int = 3000) -> Tuple[str, str]:
|
|
|
165
164
|
return fala, painel
|
|
166
165
|
|
|
167
166
|
|
|
168
|
-
# ── a palavra de ativação ────────────────────────────────────────────────
|
|
169
|
-
|
|
170
|
-
_NOMES = ("primo", "primu", "prima", "primocode", "primo code")
|
|
171
|
-
_ANTES = ("ei", "ey", "hey", "oi", "ola", "fala", "e ai", "opa", "salve")
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
def ativacao(texto: str, nome: str = "Primo") -> Tuple[bool, str]:
|
|
175
|
-
"""Começa com "Primo" (ou "ei Primo")? Devolve (sim?, o resto do pedido).
|
|
176
|
-
|
|
177
|
-
Só vale no INÍCIO da fala: "o primo da Ana ligou" não acorda ninguém."""
|
|
178
|
-
cru = str(texto or "").strip()
|
|
179
|
-
n = normalizar(cru)
|
|
180
|
-
meus = _NOMES if normalizar(nome) in ("primo", "") else (normalizar(nome),)
|
|
181
|
-
for ant in ("",) + _ANTES:
|
|
182
|
-
for nm in meus:
|
|
183
|
-
pre = (ant + " " + nm).strip()
|
|
184
|
-
if n == pre or n.startswith(pre + " "):
|
|
185
|
-
resto = re.sub(r"^\W*(?:" + r"\W+".join(re.escape(w) for w in pre.split()) + r")\W*", "",
|
|
186
|
-
_sem_acento(cru), flags=re.I)
|
|
187
|
-
# o corte vale no texto sem acento; o tamanho é o mesmo
|
|
188
|
-
resto = cru[len(cru) - len(resto):] if len(resto) <= len(cru) else resto
|
|
189
|
-
return True, resto.strip(" ,.!?;:-")
|
|
190
|
-
return False, cru
|
|
191
|
-
|
|
192
|
-
|
|
193
167
|
# ── comandos de voz que o painel e o modo resolvem sozinhos ──────────────
|
|
194
168
|
|
|
195
169
|
_COMANDOS = [
|
package/voz/primo_voz/hud.html
CHANGED
|
@@ -144,6 +144,10 @@
|
|
|
144
144
|
.leg span::before { content:""; display:inline-block; width:8px; height:8px; border-radius:50%; margin-right:6px; background:var(--marca); }
|
|
145
145
|
.leg span.p::before { background:var(--ok); }
|
|
146
146
|
.tecla { font-size:12px; color:var(--fraco); white-space:nowrap; }
|
|
147
|
+
.voltar-chat { display:inline-flex; align-items:center; gap:8px; height:38px; padding:0 16px; border:1px solid var(--borda); border-radius:999px; background:var(--fundo); color:var(--texto); font:500 13px var(--sans); cursor:pointer; white-space:nowrap; transition:border-color .2s; }
|
|
148
|
+
.voltar-chat:hover { border-color:var(--marca); }
|
|
149
|
+
.voltar-chat kbd { font:11px var(--mono); color:var(--fraco); }
|
|
150
|
+
.voltar-chat[hidden] { display:none; }
|
|
147
151
|
</style>
|
|
148
152
|
</head>
|
|
149
153
|
<body>
|
|
@@ -270,6 +274,7 @@
|
|
|
270
274
|
<div id="barras"></div>
|
|
271
275
|
<div class="leg"><span>você</span><span class="p" id="leg-nome">Primo</span></div>
|
|
272
276
|
<div class="tecla">⌨ luz do teclado no ritmo da voz</div>
|
|
277
|
+
<button class="voltar-chat" id="b-chat" hidden onclick="post({acao:'chat'})">← Voltar pro chat <kbd>esc</kbd></button>
|
|
273
278
|
</section>
|
|
274
279
|
</main>
|
|
275
280
|
|
|
@@ -529,7 +534,7 @@ function classeTela() {
|
|
|
529
534
|
function setEstado(e) {
|
|
530
535
|
if (!e || e === S.estado) return;
|
|
531
536
|
S.estado = e; classeTela(); $('nome').dataset.e = e;
|
|
532
|
-
$('estado-t').textContent = e === 'dormindo' ? '
|
|
537
|
+
$('estado-t').textContent = e === 'dormindo' ? 'clique para falar' : (ROTULO[e] || e);
|
|
533
538
|
R3.estado(e);
|
|
534
539
|
S.olhar = (e === 'pensando' || e === 'executando') ? 1 : 0;
|
|
535
540
|
$('eq').setAttribute('opacity', e === 'falando' ? 1 : 0);
|
|
@@ -557,8 +562,9 @@ function laco(t) { SP.quadro(t); if (!R3.ok && !SP.ok) animaEq(t); requestAnimat
|
|
|
557
562
|
/* ── a ponte com o Python ── */
|
|
558
563
|
window.primo = (m) => {
|
|
559
564
|
if (m.pasta !== undefined) $('pasta').textContent = m.pasta;
|
|
565
|
+
if (m.comChat) $('b-chat').hidden = false;
|
|
560
566
|
if (m.login !== undefined) $('b-entrar').hidden = !!m.login;
|
|
561
|
-
if (m.nome) { S.nome = m.nome; $('nome-h').textContent = m.nome; $('leg-nome').textContent = m.nome;
|
|
567
|
+
if (m.nome) { S.nome = m.nome; $('nome-h').textContent = m.nome; $('leg-nome').textContent = m.nome; }
|
|
562
568
|
if (m.robo === 'expressive' && !SP.ok) { if (SP.cancela) SP.cancela(); SP.falhou = true; classeTela(); }
|
|
563
569
|
if (m.evento) R3.evento(m.evento);
|
|
564
570
|
if (m.boot) montar();
|
package/voz/primo_voz/hud.py
CHANGED
|
@@ -30,8 +30,9 @@ from primo_voz.sons import Sons
|
|
|
30
30
|
AQUI = Path(__file__).resolve().parent
|
|
31
31
|
|
|
32
32
|
|
|
33
|
-
def main(demo: bool = False, entrada=None, saida=None, ao_iniciar=None, ao_sair=None) -> int:
|
|
34
|
-
"""entrada/saida/ao_iniciar/ao_sair: o modo app (app.py) liga o painel ao motor de voz sem passar por stdin/stdout.
|
|
33
|
+
def main(demo: bool = False, entrada=None, saida=None, ao_iniciar=None, ao_sair=None, chat=None) -> int:
|
|
34
|
+
"""entrada/saida/ao_iniciar/ao_sair: o modo app (app.py) liga o painel ao motor de voz sem passar por stdin/stdout.
|
|
35
|
+
chat: o app.Chat; com ele a janela abre no chat.html e a voz (hud.html) fica atrás, trocada pelo botão de onda."""
|
|
35
36
|
try:
|
|
36
37
|
from AppKit import (NSApplication, NSApplicationActivationPolicyRegular, NSImage, NSMenu, NSMenuItem, NSBackingStoreBuffered, NSColor,
|
|
37
38
|
NSEvent, NSScreen, NSWindow)
|
|
@@ -56,6 +57,40 @@ def main(demo: bool = False, entrada=None, saida=None, ao_iniciar=None, ao_sair=
|
|
|
56
57
|
def js(msg: dict) -> None:
|
|
57
58
|
fila.put("window.primo(" + json.dumps(msg, ensure_ascii=False) + ")")
|
|
58
59
|
|
|
60
|
+
fila_chat: "queue.Queue[str]" = queue.Queue()
|
|
61
|
+
fila_ui: "queue.Queue" = queue.Queue() # o que mexe na janela, rodado na thread principal
|
|
62
|
+
|
|
63
|
+
def js_chat(msg: dict) -> None:
|
|
64
|
+
fila_chat.put("window.chat&&window.chat(" + json.dumps(msg, ensure_ascii=False) + ")")
|
|
65
|
+
|
|
66
|
+
telas: dict = {}
|
|
67
|
+
|
|
68
|
+
def mostrar(qual: str) -> None:
|
|
69
|
+
"""Troca entre o chat e a voz na mesma janela, sem recarregar nenhuma das duas."""
|
|
70
|
+
if not telas:
|
|
71
|
+
return
|
|
72
|
+
telas["chat"].setHidden_(qual != "chat")
|
|
73
|
+
telas["hud"].setHidden_(qual != "hud")
|
|
74
|
+
telas["atual"] = qual
|
|
75
|
+
telas["jan"].makeFirstResponder_(telas[qual])
|
|
76
|
+
if qual == "hud":
|
|
77
|
+
js({"comChat": True})
|
|
78
|
+
js({"boot": True})
|
|
79
|
+
if chat is not None:
|
|
80
|
+
threading.Thread(target=chat.app.ligar_voz, daemon=True).start()
|
|
81
|
+
elif chat is not None: # voltou pro chat: o microfone fecha
|
|
82
|
+
threading.Thread(target=chat.app.desligar_voz, daemon=True).start()
|
|
83
|
+
|
|
84
|
+
def painel_arquivos(pastas: bool):
|
|
85
|
+
from AppKit import NSOpenPanel
|
|
86
|
+
pn_ = NSOpenPanel.openPanel()
|
|
87
|
+
pn_.setCanChooseFiles_(not pastas)
|
|
88
|
+
pn_.setCanChooseDirectories_(pastas)
|
|
89
|
+
pn_.setAllowsMultipleSelection_(not pastas)
|
|
90
|
+
if pn_.runModal() != 1:
|
|
91
|
+
return []
|
|
92
|
+
return [str(u.path()) for u in pn_.URLs()]
|
|
93
|
+
|
|
59
94
|
class Janela(NSWindow):
|
|
60
95
|
def canBecomeKeyWindow(self):
|
|
61
96
|
return True
|
|
@@ -73,6 +108,19 @@ def main(demo: bool = False, entrada=None, saida=None, ao_iniciar=None, ao_sair=
|
|
|
73
108
|
break
|
|
74
109
|
self.web.evaluateJavaScript_completionHandler_(cod, None)
|
|
75
110
|
n += 1
|
|
111
|
+
while telas:
|
|
112
|
+
try:
|
|
113
|
+
cod = fila_chat.get_nowait()
|
|
114
|
+
except queue.Empty:
|
|
115
|
+
break
|
|
116
|
+
telas["chat"].evaluateJavaScript_completionHandler_(cod, None)
|
|
117
|
+
while True:
|
|
118
|
+
try:
|
|
119
|
+
fila_ui.get_nowait()()
|
|
120
|
+
except queue.Empty:
|
|
121
|
+
break
|
|
122
|
+
except Exception as e:
|
|
123
|
+
sys.stderr.write(f"hud: {e}\n")
|
|
76
124
|
if self.jan.isMiniaturized():
|
|
77
125
|
return
|
|
78
126
|
if p.visivel != bool(self.jan.isVisible()):
|
|
@@ -124,6 +172,8 @@ def main(demo: bool = False, entrada=None, saida=None, ao_iniciar=None, ao_sair=
|
|
|
124
172
|
sys.stderr.flush()
|
|
125
173
|
elif a == "sair":
|
|
126
174
|
p.visivel = False
|
|
175
|
+
elif a == "chat": # "Voltar pro chat", na tela de voz
|
|
176
|
+
mostrar("chat")
|
|
127
177
|
elif a in ("ptt", "entrar"):
|
|
128
178
|
emitir({"acao": a})
|
|
129
179
|
elif a == "print": # esconde o HUD para o print pegar a tela de verdade
|
|
@@ -140,6 +190,52 @@ def main(demo: bool = False, entrada=None, saida=None, ao_iniciar=None, ao_sair=
|
|
|
140
190
|
js({"musica": td.musica()})
|
|
141
191
|
threading.Thread(target=_fazer, daemon=True).start()
|
|
142
192
|
|
|
193
|
+
class MsgChat(NSObject, protocols=[objc.protocolNamed("WKScriptMessageHandler")]):
|
|
194
|
+
"""A ponte do chat.html. O que mexe na janela (trocar de tela, painel de arquivos, abrir link) é daqui; o resto vai para o app.Chat."""
|
|
195
|
+
def userContentController_didReceiveScriptMessage_(self, ucc, msg):
|
|
196
|
+
try:
|
|
197
|
+
m = json.loads(str(msg.body()))
|
|
198
|
+
except ValueError:
|
|
199
|
+
return
|
|
200
|
+
a = m.get("acao")
|
|
201
|
+
if a == "erro":
|
|
202
|
+
sys.stderr.write("chat.html: " + str(m.get("arg")) + "\n")
|
|
203
|
+
sys.stderr.flush()
|
|
204
|
+
elif a == "voz": # botão de onda: abre a voz e já começa a ouvir
|
|
205
|
+
mostrar("hud")
|
|
206
|
+
emitir({"acao": "ptt"})
|
|
207
|
+
elif a == "anexar":
|
|
208
|
+
caminhos = painel_arquivos(False)
|
|
209
|
+
if caminhos:
|
|
210
|
+
threading.Thread(target=chat.anexar_caminhos, args=(caminhos,), daemon=True).start()
|
|
211
|
+
elif a == "escolher_pasta":
|
|
212
|
+
caminhos = painel_arquivos(True)
|
|
213
|
+
if caminhos:
|
|
214
|
+
threading.Thread(target=chat.escolher_pasta, args=(caminhos[0],), daemon=True).start()
|
|
215
|
+
elif a == "abrir_link":
|
|
216
|
+
from AppKit import NSWorkspace
|
|
217
|
+
u = str(m.get("url") or "")
|
|
218
|
+
if u.startswith(("http://", "https://")):
|
|
219
|
+
NSWorkspace.sharedWorkspace().openURL_(NSURL.URLWithString_(u))
|
|
220
|
+
elif a == "haptico": # a travadinha do esforço: o trackpad dá um toque em cada nível
|
|
221
|
+
try:
|
|
222
|
+
from AppKit import NSHapticFeedbackManager
|
|
223
|
+
padrao = 2 if m.get("forte") else 1 # 1 = alinhamento (leve), 2 = troca de nível (mais firme)
|
|
224
|
+
NSHapticFeedbackManager.defaultPerformer().performFeedbackPattern_performanceTime_(padrao, 1)
|
|
225
|
+
except Exception as e:
|
|
226
|
+
sys.stderr.write(f"hud: háptico: {e}\n")
|
|
227
|
+
elif a in ("abrir_arquivo", "mostrar_finder"): # só o que existe; o Mac abre com o app padrão
|
|
228
|
+
from AppKit import NSWorkspace
|
|
229
|
+
c = Path(os.path.expanduser(str(m.get("caminho") or "")))
|
|
230
|
+
if c.is_absolute() and c.exists():
|
|
231
|
+
ws = NSWorkspace.sharedWorkspace()
|
|
232
|
+
if a == "abrir_arquivo":
|
|
233
|
+
ws.openURL_(NSURL.fileURLWithPath_(str(c)))
|
|
234
|
+
else:
|
|
235
|
+
ws.activateFileViewerSelectingURLs_([NSURL.fileURLWithPath_(str(c))])
|
|
236
|
+
else:
|
|
237
|
+
chat.receber(m)
|
|
238
|
+
|
|
143
239
|
app = NSApplication.sharedApplication()
|
|
144
240
|
app.setActivationPolicy_(NSApplicationActivationPolicyRegular) # app de verdade: Dock e Cmd+Tab
|
|
145
241
|
try: # nome e ícone do Primo, mesmo fora do bundle
|
|
@@ -185,21 +281,49 @@ def main(demo: bool = False, entrada=None, saida=None, ao_iniciar=None, ao_sair=
|
|
|
185
281
|
web = WKWebView.alloc().initWithFrame_configuration_(NSMakeRect(0, 0, tela.size.width, tela.size.height), cfg)
|
|
186
282
|
web.setAutoresizingMask_(2 | 16)
|
|
187
283
|
web.setValue_forKey_(False, "drawsBackground")
|
|
188
|
-
jan.setContentView_(web)
|
|
189
284
|
url = NSURL.fileURLWithPath_(str(AQUI / "hud.html"))
|
|
190
285
|
web.loadFileURL_allowingReadAccessToURL_(url, NSURL.fileURLWithPath_(str(AQUI)))
|
|
286
|
+
if chat is None:
|
|
287
|
+
jan.setContentView_(web)
|
|
288
|
+
else: # o chat na frente; a voz atrás, já carregada (troca sem esperar)
|
|
289
|
+
from AppKit import NSView
|
|
290
|
+
fundo = NSView.alloc().initWithFrame_(NSMakeRect(0, 0, tela.size.width, tela.size.height))
|
|
291
|
+
fundo.setAutoresizingMask_(2 | 16)
|
|
292
|
+
cfg_c = WKWebViewConfiguration.alloc().init()
|
|
293
|
+
cfg_c.preferences().setValue_forKey_(True, "allowFileAccessFromFileURLs") # o vídeo/imagem/PDF citados tocam ali mesmo
|
|
294
|
+
cfg_c.userContentController().addScriptMessageHandler_name_(MsgChat.alloc().init(), "chat")
|
|
295
|
+
web_c = WKWebView.alloc().initWithFrame_configuration_(NSMakeRect(0, 0, tela.size.width, tela.size.height), cfg_c)
|
|
296
|
+
web_c.setAutoresizingMask_(2 | 16)
|
|
297
|
+
web_c.setValue_forKey_(False, "drawsBackground")
|
|
298
|
+
# leitura da pasta pessoal: os arquivos que o agente cita (~/Primo/…) aparecem no chat sem servidor
|
|
299
|
+
web_c.loadFileURL_allowingReadAccessToURL_(NSURL.fileURLWithPath_(str(AQUI / "chat.html")), NSURL.fileURLWithPath_(str(Path.home())))
|
|
300
|
+
fundo.addSubview_(web)
|
|
301
|
+
fundo.addSubview_(web_c)
|
|
302
|
+
jan.setContentView_(fundo)
|
|
303
|
+
telas.update({"chat": web_c, "hud": web, "jan": jan, "atual": "chat"})
|
|
304
|
+
web.setHidden_(True)
|
|
305
|
+
chat.js = js_chat
|
|
306
|
+
chat.mostrar_voz = lambda: fila_ui.put(lambda: mostrar("hud")) # um pedido falado com o chat na frente
|
|
191
307
|
jan.makeKeyAndOrderFront_(None)
|
|
192
308
|
app.activateIgnoringOtherApps_(True)
|
|
193
309
|
alvo = Alvo.alloc().init()
|
|
194
310
|
alvo.jan, alvo.web = jan, web
|
|
195
311
|
NSTimer.scheduledTimerWithTimeInterval_target_selector_userInfo_repeats_(1 / 30, alvo, b"tick:", None, True)
|
|
196
|
-
|
|
312
|
+
if chat is None: # sem chat a voz é a tela: o robô acorda com o som
|
|
313
|
+
threading.Timer(1.2, lambda: (js({"nome": identidade.ler()["nome"]}), js({"robo": identidade.ler()["robo"]}), js({"boot": True}), sons.tocar("boot"))).start()
|
|
314
|
+
else: # com chat: só o nome e o robô, em silêncio; o boot fica pra quando abrir a voz
|
|
315
|
+
threading.Timer(1.2, lambda: (js({"nome": identidade.ler()["nome"]}), js({"robo": identidade.ler()["robo"]}))).start()
|
|
197
316
|
|
|
198
317
|
def esconder():
|
|
199
318
|
p.visivel = False
|
|
200
319
|
|
|
201
320
|
def tecla(ev):
|
|
202
|
-
if ev.keyCode() == 53: # ESC
|
|
321
|
+
if ev.keyCode() == 53: # ESC: na voz com chat, volta pro chat; no chat, fica com a página
|
|
322
|
+
if telas:
|
|
323
|
+
if telas["atual"] == "hud":
|
|
324
|
+
mostrar("chat")
|
|
325
|
+
return None
|
|
326
|
+
return ev
|
|
203
327
|
esconder()
|
|
204
328
|
return None
|
|
205
329
|
return ev
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""identidade.py — quem é o assistente e quem é a pessoa.
|
|
2
2
|
|
|
3
3
|
O assistente se chama "Primo" até a pessoa dar outro nome ("seu nome agora é
|
|
4
|
-
Luna", ou `primocode voz nome Luna`).
|
|
4
|
+
Luna", ou `primocode voz nome Luna`). O nome NÃO ativa nada: o modo voz só abre no clique. Fica em
|
|
5
5
|
~/.primocode/voz.json, junto com o nome da pessoa e as chaves de luz e som.
|
|
6
6
|
|
|
7
7
|
Personalidade: curta, natural, humor leve. Cumprimenta pela hora, lembra o nome
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"""painel_dados.py — os dados do painel (música, calendário, clima, máquina) como um processo à parte.
|
|
2
|
+
|
|
3
|
+
No Mac, quem alimenta o hud.html é o processo da janela (hud.py: as threads `medir()` e `agenda()`). O app desktop (Electron)
|
|
4
|
+
não tem esse processo, então este ajudante roda os MESMOS laços e escreve cada mensagem como uma linha de JSON no stdout:
|
|
5
|
+
o mesmo objeto que o `js(...)` do hud.py manda pra tela (`{"musica":...}`, `{"calendario":...}`, `{"hoje":...}`, `{"clima":...}`,
|
|
6
|
+
`{"maquina":...}`). O app repassa cada linha pra `window.primo(msg)`. O hud.py não muda.
|
|
7
|
+
|
|
8
|
+
sai (stdout): uma mensagem do painel por linha (e, ao iniciar, {"nome":..} e {"robo":..} da identidade)
|
|
9
|
+
entra (stdin): ações da tela, uma por linha: {"acao":"midia","arg":"proxima"}, {"acao":"calendario"},
|
|
10
|
+
{"acao":"volume",...}, {"acao":"app",...}, {"acao":"boot"} (toca o som de abertura)
|
|
11
|
+
fecha o stdin: o ajudante encerra.
|
|
12
|
+
Rode com: python -m primo_voz.painel_dados
|
|
13
|
+
"""
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import json
|
|
17
|
+
import sys
|
|
18
|
+
import threading
|
|
19
|
+
import time
|
|
20
|
+
|
|
21
|
+
from primo_voz import identidade, tela as td
|
|
22
|
+
from primo_voz import painel as pn
|
|
23
|
+
from primo_voz.sons import Sons
|
|
24
|
+
|
|
25
|
+
# Ações da janela (o app cuida delas): o ajudante ignora.
|
|
26
|
+
DA_JANELA = {"erro", "sair", "chat", "ptt", "entrar", "print"}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def main(entrada=None, saida=None, td_=None, pn_=None, sons=None, passo: float = 1.0) -> int:
|
|
30
|
+
"""entrada/saida/td_/pn_/sons/passo: o teste troca por falsos e acelera os laços. `passo` = segundos do laço da máquina (o resto é múltiplo dele)."""
|
|
31
|
+
td_ = td_ or td
|
|
32
|
+
pn_ = pn_ or pn
|
|
33
|
+
entrada = sys.stdin if entrada is None else entrada
|
|
34
|
+
escrever = saida or sys.stdout
|
|
35
|
+
trava = threading.Lock()
|
|
36
|
+
parar = threading.Event()
|
|
37
|
+
sons = sons if sons is not None else Sons(True)
|
|
38
|
+
|
|
39
|
+
def js(msg: dict) -> None:
|
|
40
|
+
with trava:
|
|
41
|
+
try:
|
|
42
|
+
escrever.write(json.dumps(msg, ensure_ascii=False) + "\n")
|
|
43
|
+
escrever.flush()
|
|
44
|
+
except Exception:
|
|
45
|
+
parar.set() # o app fechou o cano: acabou
|
|
46
|
+
|
|
47
|
+
def espera(s: float) -> None:
|
|
48
|
+
parar.wait(s)
|
|
49
|
+
|
|
50
|
+
def medir() -> None:
|
|
51
|
+
am = pn_.Amostrador()
|
|
52
|
+
cache: dict = {}
|
|
53
|
+
k = 0
|
|
54
|
+
while not parar.is_set():
|
|
55
|
+
try:
|
|
56
|
+
js({"maquina": td_.maquina(am)})
|
|
57
|
+
js({"musica": td_.musica()})
|
|
58
|
+
if k % 20 == 0:
|
|
59
|
+
js({"clima": td_.clima_widget(cache)})
|
|
60
|
+
except Exception as e:
|
|
61
|
+
sys.stderr.write(f"painel_dados: {e}\n")
|
|
62
|
+
k += 1
|
|
63
|
+
espera(passo)
|
|
64
|
+
|
|
65
|
+
def agenda() -> None: # em outra thread: o pedido de permissão do Calendário pode demorar
|
|
66
|
+
evs: dict = {"permissao": "pendente", "eventos": []}
|
|
67
|
+
k = 0
|
|
68
|
+
falhas = 0
|
|
69
|
+
while not parar.is_set():
|
|
70
|
+
try:
|
|
71
|
+
if k % 12 == 0 or evs["permissao"] != "ok": # sem acesso, olha a cada 5 s (a pessoa pode ter liberado)
|
|
72
|
+
novo = td_.eventos_do_mes()
|
|
73
|
+
if evs["permissao"] == "ok" and novo["permissao"] != "ok":
|
|
74
|
+
falhas += 1 # erro transitório não derruba o cartão: segura os últimos eventos
|
|
75
|
+
if falhas >= 4:
|
|
76
|
+
evs, falhas = novo, 0
|
|
77
|
+
else:
|
|
78
|
+
evs, falhas = novo, 0
|
|
79
|
+
# SEMPRE manda o estado junto: o painel nunca depende de uma mensagem que pode ter chegado antes da página
|
|
80
|
+
js({"calendario": {"permissao": evs["permissao"], "dias": sorted({e["dia"] for e in evs["eventos"]})}})
|
|
81
|
+
if evs["permissao"] == "ok":
|
|
82
|
+
js({"hoje": td_.hoje_lista(evs["eventos"], time.time())})
|
|
83
|
+
except Exception as e:
|
|
84
|
+
sys.stderr.write(f"painel_dados agenda: {e}\n")
|
|
85
|
+
k += 1
|
|
86
|
+
espera(5 * passo)
|
|
87
|
+
|
|
88
|
+
def acao(m: dict) -> None:
|
|
89
|
+
try:
|
|
90
|
+
_acao(m)
|
|
91
|
+
except Exception as e: # uma ação que estoura não derruba o ajudante
|
|
92
|
+
sys.stderr.write(f"painel_dados: ação {m.get('acao')}: {e}\n")
|
|
93
|
+
|
|
94
|
+
def _acao(m: dict) -> None:
|
|
95
|
+
a = m.get("acao")
|
|
96
|
+
if a in DA_JANELA:
|
|
97
|
+
return
|
|
98
|
+
if a == "boot":
|
|
99
|
+
sons.tocar("boot")
|
|
100
|
+
elif td_.acao(m):
|
|
101
|
+
pass
|
|
102
|
+
else:
|
|
103
|
+
pn_.executar_acao(m)
|
|
104
|
+
if a == "midia":
|
|
105
|
+
espera(0.4 * passo)
|
|
106
|
+
js({"musica": td_.musica()})
|
|
107
|
+
|
|
108
|
+
try:
|
|
109
|
+
ident = identidade.ler()
|
|
110
|
+
js({"nome": ident["nome"]})
|
|
111
|
+
js({"robo": ident["robo"]})
|
|
112
|
+
except Exception as e:
|
|
113
|
+
sys.stderr.write(f"painel_dados: identidade: {e}\n")
|
|
114
|
+
threading.Thread(target=medir, daemon=True).start()
|
|
115
|
+
threading.Thread(target=agenda, daemon=True).start()
|
|
116
|
+
for linha in entrada: # stdin fechado = o app saiu ou voltou pro chat
|
|
117
|
+
if parar.is_set():
|
|
118
|
+
break
|
|
119
|
+
try:
|
|
120
|
+
m = json.loads(linha)
|
|
121
|
+
except ValueError:
|
|
122
|
+
continue
|
|
123
|
+
if not isinstance(m, dict):
|
|
124
|
+
continue
|
|
125
|
+
try:
|
|
126
|
+
threading.Thread(target=acao, args=(m,), daemon=True).start() # ação lenta (osascript) não trava o laço de leitura
|
|
127
|
+
except Exception as e:
|
|
128
|
+
sys.stderr.write(f"painel_dados: ação {m.get('acao')}: {e}\n")
|
|
129
|
+
parar.set()
|
|
130
|
+
return 0
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
if __name__ == "__main__":
|
|
134
|
+
sys.exit(main())
|