chore: adiciona .gitignore e commit.command
This commit is contained in:
@@ -0,0 +1,771 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Transcription model manager — modern macOS UI (Flet), Hex-inspired.
|
||||
|
||||
Two tabs:
|
||||
- Modelos: manage local Whisper models (download, progress, cancel, delete,
|
||||
select, open in Finder, configure storage folder).
|
||||
- Transcrição: pick a model + language, import an FCPXML/.fcpxmld project
|
||||
(e.g. dragged out of Final Cut Pro), and transcribe its media locally,
|
||||
writing a _transcript.json next to each media file.
|
||||
|
||||
Backed by the pure-Python ``fcpxml`` package. Run:
|
||||
uv run python admin/models_gui.py
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
import subprocess
|
||||
import sys
|
||||
import threading
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
import flet as ft
|
||||
|
||||
# code/ is the package root for fcpxml 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.media_intel import media_src_to_path # noqa: E402
|
||||
from fcpxml.model_manager import ( # noqa: E402
|
||||
download_model,
|
||||
get_models_dir,
|
||||
is_model_downloaded,
|
||||
list_installed_models,
|
||||
load_catalog,
|
||||
load_selected_model,
|
||||
model_cache_dir,
|
||||
save_models_dir,
|
||||
save_selected_model,
|
||||
)
|
||||
from fcpxml.parser import parse_fcpxml # noqa: E402
|
||||
from fcpxml.transcribe import transcribe # noqa: E402
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Recommended models (badge) — mirrors Hex's "Suggested" concept.
|
||||
RECOMMENDED = ("large-v3", "distil-large-v3", "small", "base")
|
||||
|
||||
# Transcription language options.
|
||||
LANGUAGES = {
|
||||
"auto": "Detectar automaticamente",
|
||||
"pt": "Português",
|
||||
"en": "Inglês",
|
||||
"es": "Espanhol",
|
||||
"fr": "Francês",
|
||||
"de": "Alemão",
|
||||
"it": "Italiano",
|
||||
"nl": "Holandês",
|
||||
"ja": "Japonês",
|
||||
"ko": "Coreano",
|
||||
"zh": "Chinês",
|
||||
}
|
||||
|
||||
# Light theme palette (macOS-like).
|
||||
ACCENT = "#007AFF"
|
||||
BG = "#F5F5F7"
|
||||
CARD = "#FFFFFF"
|
||||
BORDER = "#E5E5EA"
|
||||
TEXT = "#1D1D1F"
|
||||
SUB = "#6E6E73"
|
||||
GREEN = "#34C759"
|
||||
RED = "#FF3B30"
|
||||
PURPLE = "#AF52DE"
|
||||
|
||||
|
||||
def _stars(count: int) -> ft.Row:
|
||||
return ft.Row(
|
||||
controls=[
|
||||
ft.Icon(
|
||||
ft.Icons.STAR_ROUNDED if i < count else ft.Icons.STAR_OUTLINE_ROUNDED,
|
||||
size=15,
|
||||
color=ACCENT if i < count else BORDER,
|
||||
)
|
||||
for i in range(5)
|
||||
],
|
||||
spacing=1,
|
||||
)
|
||||
|
||||
|
||||
def _transcript_json_path(media_path: str) -> Path:
|
||||
p = Path(media_path)
|
||||
return p.with_name(p.stem + "_transcript.json")
|
||||
|
||||
|
||||
class ModelManagerApp:
|
||||
"""Flet page controller for the model manager window."""
|
||||
|
||||
def __init__(self, page: ft.Page) -> None:
|
||||
self.page = page
|
||||
self.selected = load_selected_model()
|
||||
self.downloading: Optional[str] = None
|
||||
self._cancel_events: dict[str, threading.Event] = {}
|
||||
self._picker: Optional[ft.FilePicker] = None
|
||||
|
||||
# ── helpers ────────────────────────────────────────────────────────────
|
||||
|
||||
def _refresh(self) -> None:
|
||||
self.selected = load_selected_model()
|
||||
self._rebuild_models_tab()
|
||||
self.page.update()
|
||||
|
||||
def _refresh_models_content(self) -> ft.Column:
|
||||
return self._build_model_list()
|
||||
|
||||
def _setup_picker(self) -> None:
|
||||
if self._picker is not None:
|
||||
return
|
||||
self._picker = ft.FilePicker()
|
||||
self._picker.on_result = self._on_file_picked
|
||||
self.page.overlay.append(self._picker)
|
||||
self._pending_target: Optional[dict] = None
|
||||
|
||||
def _on_file_picked(self, e) -> None:
|
||||
if self._pending_target == "project":
|
||||
if not e.files:
|
||||
return
|
||||
path = e.files[0].path
|
||||
self._project_path.value = path
|
||||
self._project_status.value = Path(path).name
|
||||
self._project_status.color = SUB
|
||||
elif self._pending_target == "models_dir":
|
||||
path = getattr(e, "path", None)
|
||||
if not path:
|
||||
return
|
||||
try:
|
||||
save_models_dir(path)
|
||||
self._dir_field.value = path
|
||||
self._dir_status.value = "Pasta de modelos atualizada ✓"
|
||||
self._dir_status.color = GREEN
|
||||
except ValueError as err:
|
||||
self._dir_status.value = str(err)
|
||||
self._dir_status.color = RED
|
||||
self._pending_target = None
|
||||
self.page.update()
|
||||
|
||||
# ── main body (tabs) ───────────────────────────────────────────────────
|
||||
|
||||
def _rebuild_body(self) -> None:
|
||||
self._setup_picker()
|
||||
self.page.controls.clear()
|
||||
models_tab = ft.Tab(
|
||||
label="Modelos",
|
||||
icon=ft.Icons.DATASET_OUTLINED,
|
||||
)
|
||||
models_tab.content = self._build_models_tab()
|
||||
transcribe_tab = ft.Tab(
|
||||
label="Transcrição",
|
||||
icon=ft.Icons.MIC_OUTLINED,
|
||||
)
|
||||
transcribe_tab.content = self._build_transcribe_tab()
|
||||
self.page.controls.append(
|
||||
ft.Tabs(
|
||||
content=[models_tab, transcribe_tab],
|
||||
length=2,
|
||||
selected_index=0,
|
||||
expand=True,
|
||||
animation_duration=200,
|
||||
)
|
||||
)
|
||||
|
||||
# ── Modelos tab ────────────────────────────────────────────────────────
|
||||
|
||||
def _build_models_tab(self) -> ft.Column:
|
||||
return ft.Column(
|
||||
controls=[
|
||||
self._build_header(),
|
||||
self._build_settings_card(),
|
||||
self._build_model_list(),
|
||||
self._build_footer(),
|
||||
],
|
||||
spacing=14,
|
||||
expand=True,
|
||||
)
|
||||
|
||||
def _rebuild_models_tab(self) -> None:
|
||||
pass # content rebuilt on demand; simplest via full _rebuild_body
|
||||
|
||||
def _build_header(self) -> ft.Container:
|
||||
return ft.Container(
|
||||
content=ft.Row(
|
||||
controls=[
|
||||
ft.Container(
|
||||
content=ft.Icon(ft.Icons.GRAPHIC_EQ, size=26, color=ft.Colors.WHITE),
|
||||
width=48,
|
||||
height=48,
|
||||
border_radius=12,
|
||||
bgcolor=ACCENT,
|
||||
alignment=ft.Alignment(0, 0),
|
||||
),
|
||||
ft.Column(
|
||||
controls=[
|
||||
ft.Text("Modelos de Transcrição", size=22, weight=ft.FontWeight.W_700, color=TEXT),
|
||||
ft.Text(
|
||||
"Escolha um modelo local para transcrever seus depoimentos. "
|
||||
"Mais precisão = mais lento e mais espaço.",
|
||||
size=13,
|
||||
color=SUB,
|
||||
),
|
||||
],
|
||||
spacing=3,
|
||||
expand=True,
|
||||
),
|
||||
],
|
||||
spacing=14,
|
||||
),
|
||||
padding=ft.Padding(4, 6, 4, 10),
|
||||
)
|
||||
|
||||
def _build_footer(self) -> ft.Container:
|
||||
return ft.Container(
|
||||
content=ft.Row(
|
||||
controls=[
|
||||
ft.Icon(ft.Icons.LOCK_OUTLINE, size=14, color=SUB),
|
||||
ft.Text(
|
||||
"Os modelos rodam localmente nesta máquina. "
|
||||
"Downloads ficam na pasta configurada acima.",
|
||||
size=11,
|
||||
color=SUB,
|
||||
),
|
||||
],
|
||||
spacing=6,
|
||||
),
|
||||
padding=ft.Padding(4, 2, 4, 2),
|
||||
)
|
||||
|
||||
def _build_settings_card(self) -> ft.Container:
|
||||
self._dir_field = ft.TextField(
|
||||
value=str(get_models_dir()),
|
||||
label="Pasta de modelos",
|
||||
hint_text="Definida via ícone ao lado",
|
||||
expand=True,
|
||||
text_size=13,
|
||||
border_radius=10,
|
||||
filled=True,
|
||||
read_only=True,
|
||||
)
|
||||
self._dir_status = ft.Text(
|
||||
f"{len(list_installed_models())} instalado(s) · "
|
||||
f"selecionado: {self.selected or 'nenhum'}",
|
||||
size=12,
|
||||
color=SUB,
|
||||
)
|
||||
|
||||
def _pick_dir(e) -> None:
|
||||
self._pending_target = "models_dir"
|
||||
self._picker.get_directory_path(
|
||||
dialog_title="Selecionar pasta de modelos",
|
||||
)
|
||||
|
||||
def _open_dir(e) -> None:
|
||||
try:
|
||||
subprocess.Popen(["open", str(get_models_dir())])
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
return ft.Container(
|
||||
content=ft.Column(
|
||||
controls=[
|
||||
ft.Row(
|
||||
controls=[
|
||||
ft.Icon(ft.Icons.FOLDER_OUTLINED, size=18, color=SUB),
|
||||
ft.Text("Local de armazenamento", size=13, weight=ft.FontWeight.W_600, color=TEXT),
|
||||
],
|
||||
spacing=8,
|
||||
),
|
||||
ft.Row(
|
||||
controls=[
|
||||
self._dir_field,
|
||||
ft.IconButton(
|
||||
ft.Icons.FOLDER_OPEN,
|
||||
tooltip="Selecionar pasta",
|
||||
on_click=_pick_dir,
|
||||
icon_color=SUB,
|
||||
),
|
||||
ft.IconButton(
|
||||
ft.Icons.OPEN_IN_NEW,
|
||||
tooltip="Abrir no Finder",
|
||||
on_click=_open_dir,
|
||||
icon_color=SUB,
|
||||
),
|
||||
],
|
||||
spacing=4,
|
||||
),
|
||||
self._dir_status,
|
||||
],
|
||||
spacing=10,
|
||||
),
|
||||
padding=16,
|
||||
border_radius=14,
|
||||
bgcolor=CARD,
|
||||
border=ft.Border.all(1, BORDER),
|
||||
shadow=ft.BoxShadow(
|
||||
blur_radius=8,
|
||||
offset=ft.Offset(0, 2),
|
||||
color=ft.Colors.with_opacity(0.06, ft.Colors.BLACK),
|
||||
),
|
||||
)
|
||||
|
||||
def _build_model_list(self) -> ft.Column:
|
||||
installed = set(list_installed_models())
|
||||
rows = []
|
||||
for catalog in load_catalog():
|
||||
name = catalog["internal_name"]
|
||||
rows.append(self._build_model_card(catalog, name in installed))
|
||||
return ft.Column(controls=rows, spacing=10, scroll=ft.ScrollMode.AUTO, expand=True)
|
||||
|
||||
def _build_model_card(self, catalog: dict, installed: bool) -> ft.Container:
|
||||
name: str = catalog["internal_name"]
|
||||
display: str = catalog["display_name"]
|
||||
is_selected = name == self.selected
|
||||
is_downloading = name == self.downloading
|
||||
is_recommended = name in RECOMMENDED
|
||||
|
||||
badges = ft.Row(spacing=6)
|
||||
if is_recommended and not installed:
|
||||
badges.controls.append(self._badge("Recomendado", ACCENT))
|
||||
if installed:
|
||||
badges.controls.append(self._badge("Instalado", GREEN))
|
||||
if is_selected:
|
||||
badges.controls.append(self._badge("Em uso", PURPLE))
|
||||
|
||||
progress = ft.ProgressBar(value=0, visible=is_downloading, width=170, color=ACCENT)
|
||||
pct = ft.Text("0%", size=12, color=SUB, visible=is_downloading)
|
||||
cancel_btn = ft.TextButton("Cancelar", visible=is_downloading, style=ft.ButtonStyle(color=RED))
|
||||
|
||||
def _cancel(name: str) -> None:
|
||||
ev = self._cancel_events.get(name)
|
||||
if ev is not None:
|
||||
ev.set()
|
||||
|
||||
cancel_btn.on_click = lambda e, n=name: _cancel(n)
|
||||
|
||||
def _make_download(name: str):
|
||||
def handler(e) -> None:
|
||||
ev = threading.Event()
|
||||
self._cancel_events[name] = ev
|
||||
self.downloading = name
|
||||
progress.visible = True
|
||||
pct.visible = True
|
||||
cancel_btn.visible = True
|
||||
self.page.update()
|
||||
threading.Thread(
|
||||
target=_run_download, args=(name, progress, pct, cancel_btn, ev), daemon=True
|
||||
).start()
|
||||
|
||||
return handler
|
||||
|
||||
def _run_download(name, progress, pct, cancel_btn, cancel_event) -> None:
|
||||
def on_progress(fraction: float) -> None:
|
||||
try:
|
||||
progress.value = fraction
|
||||
pct.value = f"{int(fraction * 100)}%"
|
||||
self.page.update()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
download_model(name, progress_cb=on_progress, cancel_event=cancel_event)
|
||||
self._cancel_events.pop(name, None)
|
||||
self.downloading = None
|
||||
installed_now = is_model_downloaded(name)
|
||||
if installed_now:
|
||||
save_selected_model(name)
|
||||
self.selected = load_selected_model()
|
||||
try:
|
||||
self._rebuild_body()
|
||||
self.page.update()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
controls_row = ft.Row(spacing=6)
|
||||
if is_downloading:
|
||||
controls_row.controls.extend([progress, pct, cancel_btn])
|
||||
elif installed:
|
||||
if is_selected:
|
||||
controls_row.controls.append(
|
||||
ft.Container(
|
||||
content=ft.Row(
|
||||
controls=[
|
||||
ft.Icon(ft.Icons.CHECK_CIRCLE_OUTLINED, size=16, color=GREEN),
|
||||
ft.Text("Em uso", size=13, weight=ft.FontWeight.W_600, color=GREEN),
|
||||
],
|
||||
spacing=4,
|
||||
),
|
||||
padding=ft.Padding(10, 6, 10, 6),
|
||||
border_radius=8,
|
||||
border=ft.Border.all(1, GREEN),
|
||||
)
|
||||
)
|
||||
else:
|
||||
controls_row.controls.append(
|
||||
ft.FilledButton(
|
||||
"Selecionar",
|
||||
on_click=lambda e, n=name: self._select(n),
|
||||
style=ft.ButtonStyle(bgcolor=ACCENT, color=ft.Colors.WHITE),
|
||||
)
|
||||
)
|
||||
controls_row.controls.append(
|
||||
ft.IconButton(
|
||||
ft.Icons.FOLDER_OPEN,
|
||||
tooltip="Mostrar no Finder",
|
||||
on_click=lambda e, n=name: self._open_model(n),
|
||||
icon_color=SUB,
|
||||
)
|
||||
)
|
||||
controls_row.controls.append(
|
||||
ft.IconButton(
|
||||
ft.Icons.DELETE_OUTLINE,
|
||||
tooltip="Remover download",
|
||||
on_click=lambda e, n=name: self._delete(n),
|
||||
icon_color=RED,
|
||||
)
|
||||
)
|
||||
else:
|
||||
controls_row.controls.append(
|
||||
ft.FilledButton(
|
||||
"Download",
|
||||
on_click=_make_download(name),
|
||||
style=ft.ButtonStyle(bgcolor=ACCENT, color=ft.Colors.WHITE),
|
||||
)
|
||||
)
|
||||
|
||||
card_border = (
|
||||
ft.Border.all(1.4, ACCENT) if is_recommended and not installed else ft.Border.all(1, BORDER)
|
||||
)
|
||||
|
||||
return ft.Container(
|
||||
content=ft.Column(
|
||||
controls=[
|
||||
ft.Row(
|
||||
controls=[
|
||||
ft.Text(display, size=16, weight=ft.FontWeight.W_600, color=TEXT, expand=True),
|
||||
badges,
|
||||
],
|
||||
alignment=ft.MainAxisAlignment.SPACE_BETWEEN,
|
||||
),
|
||||
ft.Row(
|
||||
controls=[
|
||||
ft.Icon(ft.Icons.PUBLIC_OUTLINED, size=13, color=SUB),
|
||||
ft.Text(catalog.get("size", ""), size=12, color=SUB),
|
||||
ft.Text("·", size=12, color=BORDER),
|
||||
ft.Icon(ft.Icons.STORAGE_OUTLINED, size=13, color=SUB),
|
||||
ft.Text(catalog.get("storage", ""), size=12, color=SUB),
|
||||
],
|
||||
spacing=6,
|
||||
),
|
||||
ft.Row(
|
||||
controls=[
|
||||
_stars(int(catalog.get("accuracy", 0))),
|
||||
ft.Text("Precisão", size=11, color=SUB),
|
||||
ft.Container(width=14),
|
||||
_stars(int(catalog.get("speed", 0))),
|
||||
ft.Text("Velocidade", size=11, color=SUB),
|
||||
],
|
||||
spacing=6,
|
||||
),
|
||||
ft.Row(
|
||||
controls=[controls_row],
|
||||
alignment=ft.MainAxisAlignment.END,
|
||||
),
|
||||
],
|
||||
spacing=10,
|
||||
),
|
||||
padding=16,
|
||||
border_radius=14,
|
||||
bgcolor=ft.Colors.with_opacity(0.55, CARD) if installed else CARD,
|
||||
border=card_border,
|
||||
shadow=ft.BoxShadow(
|
||||
blur_radius=8,
|
||||
offset=ft.Offset(0, 2),
|
||||
color=ft.Colors.with_opacity(0.05, ft.Colors.BLACK),
|
||||
),
|
||||
)
|
||||
|
||||
def _badge(self, text: str, color: str) -> ft.Container:
|
||||
return ft.Container(
|
||||
content=ft.Text(text, size=10, weight=ft.FontWeight.W_700, color=color),
|
||||
padding=ft.Padding(8, 3, 8, 3),
|
||||
border_radius=8,
|
||||
bgcolor=ft.Colors.with_opacity(0.12, color),
|
||||
)
|
||||
|
||||
def _select(self, name: str) -> None:
|
||||
if is_model_downloaded(name):
|
||||
save_selected_model(name)
|
||||
self._rebuild_body()
|
||||
self.page.update()
|
||||
|
||||
def _delete(self, name: str) -> None:
|
||||
import shutil
|
||||
|
||||
try:
|
||||
shutil.rmtree(model_cache_dir(name), ignore_errors=True)
|
||||
except Exception:
|
||||
pass
|
||||
self._rebuild_body()
|
||||
self.page.update()
|
||||
|
||||
def _open_model(self, name: str) -> None:
|
||||
try:
|
||||
subprocess.Popen(["open", str(model_cache_dir(name))])
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
# ── Transcrição tab ────────────────────────────────────────────────────
|
||||
|
||||
def _build_transcribe_tab(self) -> ft.Column:
|
||||
# Model selection: installed models first, then all.
|
||||
installed = list_installed_models()
|
||||
model_options = [
|
||||
ft.dropdown.Option(m, text=m) for m in installed
|
||||
]
|
||||
for catalog in load_catalog():
|
||||
m = catalog["internal_name"]
|
||||
if m not in installed:
|
||||
model_options.append(ft.dropdown.Option(m, text=m))
|
||||
|
||||
model_default = self.selected if self.selected in installed else (installed[0] if installed else None)
|
||||
model_dd = ft.Dropdown(
|
||||
label="Modelo",
|
||||
options=model_options,
|
||||
value=model_default,
|
||||
expand=True,
|
||||
text_size=13,
|
||||
border_radius=10,
|
||||
)
|
||||
lang_dd = ft.Dropdown(
|
||||
label="Idioma",
|
||||
options=[ft.dropdown.Option(k, text=v) for k, v in LANGUAGES.items()],
|
||||
value="auto",
|
||||
expand=True,
|
||||
text_size=13,
|
||||
border_radius=10,
|
||||
)
|
||||
|
||||
self._project_path = ft.TextField(
|
||||
label="Projeto FCPXML",
|
||||
hint_text="Arraste o arquivo .fcpxml / .fcpxmld ou selecione abaixo",
|
||||
expand=True,
|
||||
read_only=True,
|
||||
text_size=13,
|
||||
border_radius=10,
|
||||
)
|
||||
self._project_status = ft.Text("Nenhum projeto selecionado", size=12, color=SUB)
|
||||
|
||||
def _pick_project(e) -> None:
|
||||
self._pending_target = "project"
|
||||
self._picker.pick_files(
|
||||
dialog_title="Selecionar projeto FCPXML",
|
||||
allow_multiple=False,
|
||||
allowed_extensions=["fcpxml", "fcpxmld", "xml"],
|
||||
)
|
||||
|
||||
progress_bar = ft.ProgressBar(value=0, visible=False, color=ACCENT)
|
||||
status = ft.Text("", size=12, color=SUB)
|
||||
result_box = ft.Container(
|
||||
content=ft.Column(
|
||||
controls=[ft.Text("", size=13, color=TEXT)],
|
||||
spacing=8,
|
||||
),
|
||||
visible=False,
|
||||
padding=14,
|
||||
border_radius=10,
|
||||
bgcolor=CARD,
|
||||
border=ft.Border.all(1, BORDER),
|
||||
)
|
||||
|
||||
def _transcribe_run(e) -> None:
|
||||
proj_path = self._project_path.value
|
||||
if not proj_path:
|
||||
status.value = "Selecione um projeto FCPXML primeiro."
|
||||
status.color = RED
|
||||
self.page.update()
|
||||
return
|
||||
model = model_dd.value or "base"
|
||||
lang = lang_dd.value
|
||||
if lang == "auto":
|
||||
lang = None
|
||||
status.value = f"Transcrevendo com {model}… isso pode levar alguns minutos."
|
||||
status.color = SUB
|
||||
progress_bar.visible = True
|
||||
progress_bar.value = 0
|
||||
result_box.visible = False
|
||||
self.page.update()
|
||||
threading.Thread(
|
||||
target=_run, args=(proj_path, model, lang, progress_bar, status, result_box), daemon=True
|
||||
).start()
|
||||
|
||||
def _run(proj_path, model, lang, progress_bar, status, result_box) -> None:
|
||||
try:
|
||||
proj = parse_fcpxml(proj_path)
|
||||
except Exception as exc:
|
||||
_set_status(f"Erro ao ler o projeto: {exc}", RED)
|
||||
return
|
||||
tl = proj.primary_timeline or (proj.timelines[0] if proj.timelines else None)
|
||||
if tl is None or not getattr(tl, "clips", None):
|
||||
_set_status("Nenhum clip de mídia encontrado no projeto.", RED)
|
||||
return
|
||||
media_paths = []
|
||||
for clip in tl.clips:
|
||||
mp = media_src_to_path(clip.media_path or "")
|
||||
if mp and Path(mp).is_file():
|
||||
if mp not in media_paths:
|
||||
media_paths.append(mp)
|
||||
if not media_paths:
|
||||
_set_status("Nenhum arquivo de mídia acessível encontrado.", RED)
|
||||
return
|
||||
|
||||
total = len(media_paths)
|
||||
results = []
|
||||
for i, mp in enumerate(media_paths, 1):
|
||||
_set_progress(i / total)
|
||||
json_path = _transcript_json_path(mp)
|
||||
if json_path.is_file():
|
||||
try:
|
||||
data = json.loads(json_path.read_text(encoding="utf-8"))
|
||||
if isinstance(data, dict) and isinstance(data.get("words"), list):
|
||||
results.append((mp, data))
|
||||
continue
|
||||
except (OSError, ValueError):
|
||||
pass
|
||||
data = transcribe(mp, model_size=model, language=lang)
|
||||
if data is None:
|
||||
_set_status(f"Não foi possível transcrever: {Path(mp).name}", RED)
|
||||
return
|
||||
try:
|
||||
json_path.write_text(
|
||||
json.dumps({"source": Path(mp).name, **data}, ensure_ascii=False, indent=2),
|
||||
encoding="utf-8",
|
||||
)
|
||||
except OSError as exc:
|
||||
_set_status(f"Não foi possível salvar o JSON: {exc}", RED)
|
||||
return
|
||||
results.append((mp, data))
|
||||
|
||||
# Render summary.
|
||||
lines = [f"Transcrição concluída — {len(results)} arquivo(s)."]
|
||||
total_words = 0
|
||||
for mp, data in results:
|
||||
nw = len(data.get("words", []))
|
||||
total_words += nw
|
||||
lines.append(f"• {Path(mp).name} — {nw} palavras")
|
||||
lines.append(f"\nTotal: {total_words} palavras.")
|
||||
result_box.content.controls[0].value = "\n".join(lines)
|
||||
result_box.visible = True
|
||||
_set_progress(1.0)
|
||||
_set_status("Concluído ✓. Transcrições salvas como _transcript.json ao lado de cada mídia.")
|
||||
|
||||
def _set_progress(v: float) -> None:
|
||||
try:
|
||||
progress_bar.value = v
|
||||
self.page.update()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _set_status(msg: str, color: str) -> None:
|
||||
try:
|
||||
status.value = msg
|
||||
status.color = color
|
||||
progress_bar.visible = False
|
||||
self.page.update()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return ft.Column(
|
||||
controls=[
|
||||
self._build_transcribe_header(),
|
||||
ft.Container(
|
||||
content=ft.Column(
|
||||
controls=[
|
||||
ft.Text("Configuração", size=13, weight=ft.FontWeight.W_600, color=TEXT),
|
||||
ft.Row(controls=[model_dd, lang_dd], spacing=10),
|
||||
ft.Row(
|
||||
controls=[
|
||||
self._project_path,
|
||||
ft.FilledButton(
|
||||
"Procurar…",
|
||||
on_click=_pick_project,
|
||||
style=ft.ButtonStyle(bgcolor=ACCENT, color=ft.Colors.WHITE),
|
||||
),
|
||||
],
|
||||
spacing=8,
|
||||
),
|
||||
self._project_status,
|
||||
progress_bar,
|
||||
ft.FilledButton(
|
||||
"Transcrever",
|
||||
on_click=_transcribe_run,
|
||||
style=ft.ButtonStyle(bgcolor=ACCENT, color=ft.Colors.WHITE),
|
||||
),
|
||||
status,
|
||||
result_box,
|
||||
],
|
||||
spacing=12,
|
||||
),
|
||||
padding=18,
|
||||
border_radius=14,
|
||||
bgcolor=CARD,
|
||||
border=ft.Border.all(1, BORDER),
|
||||
shadow=ft.BoxShadow(
|
||||
blur_radius=8,
|
||||
offset=ft.Offset(0, 2),
|
||||
color=ft.Colors.with_opacity(0.06, ft.Colors.BLACK),
|
||||
),
|
||||
),
|
||||
],
|
||||
spacing=14,
|
||||
expand=True,
|
||||
)
|
||||
|
||||
def _build_transcribe_header(self) -> ft.Container:
|
||||
return ft.Container(
|
||||
content=ft.Row(
|
||||
controls=[
|
||||
ft.Container(
|
||||
content=ft.Icon(ft.Icons.MIC, size=26, color=ft.Colors.WHITE),
|
||||
width=48,
|
||||
height=48,
|
||||
border_radius=12,
|
||||
bgcolor=ACCENT,
|
||||
alignment=ft.Alignment(0, 0),
|
||||
),
|
||||
ft.Column(
|
||||
controls=[
|
||||
ft.Text("Transcrição", size=22, weight=ft.FontWeight.W_700, color=TEXT),
|
||||
ft.Text(
|
||||
"Importe seu projeto do Final Cut Pro, escolha modelo e idioma, "
|
||||
"e transcreva o depoimento localmente.",
|
||||
size=13,
|
||||
color=SUB,
|
||||
),
|
||||
],
|
||||
spacing=3,
|
||||
expand=True,
|
||||
),
|
||||
],
|
||||
spacing=14,
|
||||
),
|
||||
padding=ft.Padding(4, 6, 4, 10),
|
||||
)
|
||||
|
||||
|
||||
def main(page: ft.Page) -> None:
|
||||
page.title = "Modelos de Transcrição"
|
||||
page.window.width = 660
|
||||
page.window.height = 800
|
||||
page.window.min_width = 480
|
||||
page.window.min_height = 480
|
||||
page.theme_mode = ft.ThemeMode.LIGHT
|
||||
page.bgcolor = BG
|
||||
page.padding = 20
|
||||
page.spacing = 14
|
||||
|
||||
app = ModelManagerApp(page)
|
||||
app._rebuild_body()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
ft.app(main)
|
||||
Reference in New Issue
Block a user