fix: admin/api apontava para admin/code (inexistente) — crash no app
Ao dividir _shared.py em admin/api/*.py ontem, o cálculo `Path(__file__).resolve().parent.parent / "code"` foi copiado sem ajustar para o nível de diretório novo. No arquivo original (admin/models_api.py, direto em admin/) dois `.parent` chegavam na raiz do repo. Em admin/api/shared.py, um nível mais fundo, dois `.parent` param em admin/ — e admin/code nunca existiu. sys.path nunca recebia code/, então toda ação que passa por `server` (analisar voz, aplicar decisões) crashava o app com ModuleNotFoundError: server_tools. O bug sobreviveu a duas rodadas de validação da sessão anterior — lint zero, 1454 testes verdes, comando testado manualmente pela ponte — porque todos rodam num venv com install editável (__editable__.fcp_mcp_server.pth) que já deixa fcpxml/server_tools importáveis por conta própria, mascarando qualquer erro no cálculo manual de sys.path. Só o app real, no fallback sem uv, expõe o bug. Correção: o cálculo de sys.path sai de cada módulo de comando (estava duplicado em nove arquivos) e passa a existir uma única vez em admin/api/__init__.py, que roda antes de qualquer submódulo — nenhum precisa mais da própria cópia. O teste de regressão precisou de duas tentativas pelo mesmo motivo do bug: a primeira versão também passava com o bug presente, por rodar no mesmo venv "de sorte". Só ficou confiável isolando um subprocess que remove site-packages do sys.path antes de importar — confirmado nos dois sentidos, falha com o bug reintroduzido e passa com a correção (TestCodeDirResolution). Detalhe completo, incluindo por que o comando manual não pegou: Engine/docs/05_EXPERIENCIAS.md #25. Lint zerado, 1457 testes passando (3 novos), app compilado. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
cbd9297751
commit
711c397dfe
+1
-165
@@ -16,173 +16,9 @@ import threading
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
# code/ is the package root for fcpxml and server modules.
|
||||
_CODE_DIR = str(Path(__file__).resolve().parent.parent / "code")
|
||||
if _CODE_DIR not in sys.path:
|
||||
sys.path.insert(0, _CODE_DIR)
|
||||
|
||||
from fcpxml.diarize import build_speakers
|
||||
from fcpxml.media_intel import media_src_to_path
|
||||
from fcpxml.parser import parse_fcpxml
|
||||
from fcpxml.diarize import build_speakers # noqa: E402
|
||||
|
||||
"""JSON bridge between the SwiftUI app and the fcp-mcp-server Python engine.
|
||||
|
||||
The SwiftUI app (MacApp/) launches this script as a subprocess with a command
|
||||
and optional JSON arguments, then reads a single JSON document (or
|
||||
newline-delimited JSON for progress) on stdout.
|
||||
|
||||
Commands:
|
||||
catalog
|
||||
-> {"models": [{display_name, internal_name, size, storage,
|
||||
accuracy, speed}], "installed": [names],
|
||||
"selected": name, "models_dir": path, "installed_count": n,
|
||||
"recommended": [names]}
|
||||
|
||||
download {"model": "small"}
|
||||
-> JSON-lines: {"type":"progress","fraction":0.42}
|
||||
{"type":"done","installed":true}
|
||||
{"type":"error","message":"..."}
|
||||
|
||||
cancel {"model": "small"}
|
||||
-> {"ok": true}
|
||||
|
||||
select {"model": "small"}
|
||||
-> {"ok": true, "selected": "small"}
|
||||
|
||||
set_language {"language": "pt"} | "auto"
|
||||
-> {"ok": true, "language": "pt"}
|
||||
|
||||
delete {"model": "small"}
|
||||
-> {"ok": true}
|
||||
|
||||
open_finder {"model": "small"}
|
||||
-> {"ok": true}
|
||||
|
||||
set_models_dir {"dir": "/path"}
|
||||
-> {"ok": true, "models_dir": "/path"}
|
||||
|
||||
inspect {"path": "/path/to/project.fcpxml"}
|
||||
-> {"ok": true, "path": "...", "name": "...", "fcpxml_version": "1.13",
|
||||
"timelines": [{name, duration_seconds, frame_rate, width, height,
|
||||
clips, cuts, connected, markers}]}
|
||||
or {"ok": false, "error": "..."}
|
||||
|
||||
analyze_voice {"path": "...", "output_dir": "...", "model": "...",
|
||||
"language": "pt"|"auto"|null, "hf_token": "..."|null,
|
||||
"num_speakers": ""|null}
|
||||
Build the voice timeline (transcript+diarization+acoustics) for
|
||||
every unique source media — analysis only, writes _voice_timeline.json
|
||||
next to each media, `path` passes through unchanged. Meant as one
|
||||
entry in the batch operations list (see processBatchStep), so
|
||||
`refine_voice_timeline` never has to reopen the audio later.
|
||||
-> {"ok": true, "path": "...", "message": "..."} or {"ok": false, "error": "..."}
|
||||
|
||||
build_phrase_review {"voice_timeline": "..._voice_timeline.json",
|
||||
"actions": {...}|[...]|null, "fresh": false}
|
||||
The reviewable script for the wizard's emphasis step: every phrase with
|
||||
the AI's decision already applied (active/emphasis/trim). A review saved
|
||||
earlier for the same timeline is returned as-is unless `fresh` is true.
|
||||
-> {"ok": true, "reused": bool, "source", "duration", "speakers",
|
||||
"phrases": [{index, start, end, trim_start, trim_end, text, speaker,
|
||||
active, emphasis (0-3), track, peak_emphasis,
|
||||
take_boundary, gap_before, reason, words}],
|
||||
"errors": [...]}
|
||||
|
||||
save_phrase_review {"voice_timeline": "...", "phrases": [...], "source": "...",
|
||||
"duration": 0.0, "speakers": [...]}
|
||||
Writes _phrase_review.json plus the _phrase_actions.json derived from it.
|
||||
-> {"ok": true, "review_path", "actions_path", "emphasis_count",
|
||||
"removed_count"}
|
||||
|
||||
dynamic_subtitle_config {}
|
||||
-> {"ok": true, "band_height", "block_center_y", "line_gap", "font",
|
||||
"font_size", "emphasis_font", "emphasis_face", "emphasis_size",
|
||||
"active_color", "emphasis_color", "text_scale"}
|
||||
|
||||
set_dynamic_subtitle_config {<any of the fields above>}
|
||||
Persists only the given fields to ~/.fcp-mcp-server/config.json.
|
||||
generate_dynamic_subtitles reads this as its own fallback default.
|
||||
-> {"ok": true, <same shape as dynamic_subtitle_config>}
|
||||
|
||||
silence_config {}
|
||||
-> {"ok": true, "noise_db": -30.0, "min_silence": 0.5, "padding": 0.05}
|
||||
|
||||
set_silence_config {"noise_db": -30.0, "min_silence": 0.5, "padding": 0.05}
|
||||
Persists only the given fields. detect_media_silence and
|
||||
remove_media_silence read this as their own fallback default.
|
||||
-> {"ok": true, <same shape as silence_config>}
|
||||
|
||||
transcribe {"path": "...", "model": "small", "language": "pt"|null,
|
||||
"hf_token": "..."|null, "num_speakers": ""|null}
|
||||
-> JSON-lines:
|
||||
{"type":"progress","fraction":0.5,"stage":"Transcrevendo..."}
|
||||
{"type":"result","transcripts":[{"media","language","words",
|
||||
"duration","preview","saved",
|
||||
"speakers"}]}
|
||||
{"type":"error","message":"..."}
|
||||
|
||||
edit_by_transcript {"path": "...", "phrases": ["frase um", "frase dois"],
|
||||
"mode": "remove"|"keep_only", "clip_name": "..."|null,
|
||||
"padding": 0.0, "model": "small", "language": "pt"|null}
|
||||
-> {"ok": true, "path": "..._transcript_edit.fcpxml", "message": "..."}
|
||||
or {"ok": false, "error": "..."}
|
||||
|
||||
remove_filler_words {"path": "...", "fillers": ["um","uh"]|null,
|
||||
"clip_name": "..."|null, "padding": 0.02,
|
||||
"model": "small", "language": "pt"|null}
|
||||
-> {"ok": true, "path": "..._defillered.fcpxml", "message": "..."}
|
||||
or {"ok": false, "error": "..."}
|
||||
|
||||
transcript_markers {"path": "...", "clip_name": "..."|null,
|
||||
"marker_type": "chapter", "max_label_length": 50,
|
||||
"model": "small", "language": "pt"|null}
|
||||
-> {"ok": true, "path": "..._transcript_markers.fcpxml", "message": "..."}
|
||||
or {"ok": false, "error": "..."}
|
||||
|
||||
add_zoom {"path": "...", "clip_id": "...", "start": 10.0, "end": 16.0,
|
||||
"scale": 1.3, "ease": 0.3, "position": "0 0"|null}
|
||||
-> {"ok": true, "path": "..._zoom.fcpxml", "message": "..."}
|
||||
or {"ok": false, "error": "..."}
|
||||
|
||||
generate_dynamic_subtitles {"path": "...", "clip_name": "..."|null,
|
||||
"band_height": 0.22, "block_center_y": -167,
|
||||
"font": "Helvetica Neue", "font_size": 128,
|
||||
"emphasis_font": "Playfair Display",
|
||||
"emphasis_face": "Medium Italic", "emphasis_size": 265,
|
||||
"active_color": "1 1 1 1", "emphasis_color": "1 1 1 1",
|
||||
"model": "small", "language": "pt"|null}
|
||||
-> {"ok": true, "path": "..._dynamic_subtitles.fcpxml", "message": "..."}
|
||||
or {"ok": false, "error": "..."}
|
||||
|
||||
rename_speakers {"path": "/to/media_transcript.json",
|
||||
"speakers": {"SPEAKER_01": "Nome"}}
|
||||
-> {"ok": true, "speakers": [...]}
|
||||
|
||||
set_diarization {"token": "hf_...", "num_speakers": ""}
|
||||
-> {"ok": true, "diarization": bool, "diarization_message": "...",
|
||||
"num_speakers": "..."}
|
||||
|
||||
acoustics_capability
|
||||
Whether librosa (pitch/energy for voice analysis) is installed.
|
||||
-> {"ok": true, "available": bool, "message": "..."}
|
||||
|
||||
voice_analysis
|
||||
-> {"ok": true, "energy_threshold": 0.5, "emphasis_threshold": 0.85,
|
||||
"emphasis_weights": {...}, "emotion_enabled": false,
|
||||
"emotion_sensitivity": 0.5}
|
||||
|
||||
set_voice_analysis {"energy_threshold": 0.6, "emphasis_threshold": 0.9,
|
||||
"emphasis_weights": {"energy": 0.4}|null,
|
||||
"emotion_enabled": true, "emotion_sensitivity": 0.5}
|
||||
-> same shape as voice_analysis (only given fields change)
|
||||
|
||||
Exit code 0 on success, 1 on error.
|
||||
"""
|
||||
|
||||
_CODE_DIR = str(Path(__file__).resolve().parent.parent / "code")
|
||||
|
||||
if _CODE_DIR not in sys.path:
|
||||
sys.path.insert(0, _CODE_DIR)
|
||||
|
||||
RECOMMENDED = ("large-v3", "distil-large-v3", "small", "base")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user