first commit
This commit is contained in:
commit
7e405f7354
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
opencode
|
||||
.env
|
||||
250
Transcription_Bot.py
Normal file
250
Transcription_Bot.py
Normal file
@ -0,0 +1,250 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
transcribe_bot.py – fool‑proof transcription & summarization helper
|
||||
────────────────────────────────────────────────────────────────────
|
||||
Updates v1.4 (2025‑07‑10)
|
||||
• **UX** – Replaced basic text prompts with interactive `questionary` prompts.
|
||||
• **Project** – Added `pyproject.toml` to manage dependencies like `rich` and `questionary`.
|
||||
• **Output** – Now using `rich` for pretty-printing.
|
||||
• **Workflow** – Ask for output dir once, then process multiple files.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
import os
|
||||
import sys
|
||||
import shutil
|
||||
import subprocess
|
||||
import platform
|
||||
from pathlib import Path
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Helper wrappers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def command_exists(cmd: str) -> bool:
|
||||
return shutil.which(cmd) is not None
|
||||
|
||||
|
||||
def run_ps(cmd: str, *, check: bool = True):
|
||||
"""Run *cmd* inside a PowerShell session (Windows only)."""
|
||||
if os.name != "nt":
|
||||
subprocess.run(cmd, shell=True, check=check)
|
||||
return
|
||||
subprocess.run([
|
||||
"powershell", "-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", cmd
|
||||
], check=check)
|
||||
|
||||
|
||||
def pip_install(*args: str, allow_fail: bool = False) -> bool:
|
||||
try:
|
||||
subprocess.check_call([sys.executable, "-m", "pip", "install", *args])
|
||||
return True
|
||||
except subprocess.CalledProcessError as e:
|
||||
if allow_fail:
|
||||
return False
|
||||
raise e
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Python version sanity check
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
MIN_PY = (3, 8)
|
||||
MAX_PY = (3, 12)
|
||||
|
||||
if not (MIN_PY <= sys.version_info[:2] <= MAX_PY):
|
||||
sys.exit(
|
||||
f"[FATAL] Python {platform.python_version()} isn’t supported. Use 3.{MIN_PY[1]}–3.{MAX_PY[1]} (3.10‑3.12 recommended)."
|
||||
)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Dependency installers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
SCOOP_INSTALL_CMD = (
|
||||
"Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser; "
|
||||
"Invoke-RestMethod -Uri https://get.scoop.sh | Invoke-Expression"
|
||||
)
|
||||
|
||||
|
||||
def ensure_scoop():
|
||||
if os.name != "nt":
|
||||
return
|
||||
if command_exists("scoop"):
|
||||
return
|
||||
rich.print("[yellow]Scoop not found, installing...[/yellow]")
|
||||
run_ps(SCOOP_INSTALL_CMD)
|
||||
|
||||
|
||||
def ensure_scoop_bucket(bucket: str):
|
||||
if os.name != "nt":
|
||||
return
|
||||
run_ps(f"scoop bucket add {bucket}", check=False)
|
||||
|
||||
|
||||
def ensure_ffmpeg():
|
||||
if command_exists("ffmpeg"):
|
||||
return
|
||||
rich.print("[yellow]ffmpeg not found, installing...[/yellow]")
|
||||
if os.name == "nt":
|
||||
run_ps("scoop install ffmpeg")
|
||||
else:
|
||||
pip_install("ffmpeg-python")
|
||||
|
||||
|
||||
def ensure_rust():
|
||||
if command_exists("cargo"):
|
||||
return
|
||||
rich.print("[yellow]Rust tool-chain not found, installing...[/yellow]")
|
||||
if os.name == "nt":
|
||||
run_ps("scoop install rust")
|
||||
else:
|
||||
subprocess.run("curl https://sh.rustup.rs -sSf | sh -s -- -y", shell=True, check=True)
|
||||
os.environ["PATH"] += os.pathsep + str(Path.home() / ".cargo" / "bin")
|
||||
|
||||
|
||||
def ensure_python_pkg(pypi_name: str):
|
||||
try:
|
||||
subprocess.check_call(
|
||||
[sys.executable, "-m", "pip", "show", pypi_name],
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL,
|
||||
)
|
||||
except subprocess.CalledProcessError:
|
||||
rich.print(f"[yellow]Python package [bold]{pypi_name}[/bold] not found, installing...[/yellow]")
|
||||
pip_install(pypi_name)
|
||||
|
||||
|
||||
def ensure_ollama():
|
||||
if command_exists("ollama"):
|
||||
return
|
||||
rich.print("[bold red]Ollama CLI not in PATH.[/bold red]")
|
||||
rich.print("Please install from [blue]https://ollama.ai[/blue] and ensure it's in your PATH.")
|
||||
sys.exit(1)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Whisper + Ollama API helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
GEMMA_WRAP = "<start_of_turn>user\n{prompt}<end_of_turn>\n<start_of_turn>model\n"
|
||||
|
||||
|
||||
def ollama_run(prompt: str) -> str:
|
||||
"""Run Gemma via Ollama and return stdout decoded as UTF‑8."""
|
||||
wrapped = GEMMA_WRAP.format(prompt=prompt)
|
||||
proc = subprocess.run(
|
||||
["ollama", "run", "deepseek-r1:1.5b"],
|
||||
input=wrapped.encode("utf-8"),
|
||||
capture_output=True,
|
||||
check=True,
|
||||
)
|
||||
return proc.stdout.decode("utf-8", "replace").strip()
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Transcription
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def transcribe_audio(src: Path, job_dir: Path) -> str:
|
||||
import whisper
|
||||
|
||||
rich.print("[cyan]Transcribing... (hang tight, this may take a while)[/cyan]")
|
||||
model = whisper.load_model("base")
|
||||
result = model.transcribe(str(src))
|
||||
text = result["text"].strip()
|
||||
(job_dir / "transcription.txt").write_text(text, encoding="utf-8")
|
||||
rich.print(f"[green]✓ Transcript saved to {job_dir / 'transcription.txt'}[/green]")
|
||||
return text
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# CLI helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def ask_path(msg: str, *, must_exist=False, file_ok=False) -> Path:
|
||||
path = questionary.path(
|
||||
message=msg,
|
||||
validate=lambda p: True if not must_exist or os.path.exists(p) else "Path does not exist.",
|
||||
file_filter=lambda p: True if not file_ok or os.path.isfile(p) else "Path is not a file.",
|
||||
).ask()
|
||||
if path is None:
|
||||
raise KeyboardInterrupt
|
||||
return Path(path)
|
||||
|
||||
|
||||
def ask_menu(msg: str, choices: list[str]) -> str:
|
||||
choice = questionary.select(message=msg, choices=choices).ask()
|
||||
if choice is None:
|
||||
raise KeyboardInterrupt
|
||||
return choice
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Dependency orchestration
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def setup_deps():
|
||||
rich.print("[bold]———— Checking dependencies ————[/bold]")
|
||||
ensure_scoop()
|
||||
ensure_scoop_bucket("extras")
|
||||
ensure_ffmpeg()
|
||||
ensure_rust()
|
||||
ensure_python_pkg("openai-whisper")
|
||||
ensure_python_pkg("setuptools-rust")
|
||||
ensure_python_pkg("rich")
|
||||
ensure_python_pkg("questionary")
|
||||
ensure_ollama()
|
||||
rich.print("[bold green]✓ All set![/bold green]\n")
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Main loop
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def main():
|
||||
setup_deps()
|
||||
out_dir = ask_path("Select an output directory:", must_exist=False)
|
||||
out_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
while True:
|
||||
src = ask_path("Select an audio/video file to process:", must_exist=True, file_ok=True)
|
||||
job_dir = out_dir / src.stem
|
||||
job_dir.mkdir(exist_ok=True)
|
||||
|
||||
transcript = transcribe_audio(src, job_dir)
|
||||
|
||||
while True:
|
||||
choice = ask_menu(
|
||||
"What next?",
|
||||
choices=["Exit", "Summarize transcript", "Custom prompt"],
|
||||
)
|
||||
if choice == "Exit":
|
||||
rich.print("[bold magenta]Farewell ✌️[/bold magenta]")
|
||||
sys.exit()
|
||||
elif choice in {"Summarize transcript", "Custom prompt"}:
|
||||
if choice == "Summarize transcript":
|
||||
user_prompt = "You are a summarization bot, please observe the contents of this transcription and provide a concise paragraph summary."
|
||||
else:
|
||||
user_prompt = questionary.text("Enter your prompt for Gemma:").ask()
|
||||
if user_prompt is None:
|
||||
raise KeyboardInterrupt
|
||||
|
||||
reply = ollama_run(f"{user_prompt}\n\n{transcript}")
|
||||
(job_dir / "processed.txt").write_text(reply, encoding="utf-8")
|
||||
rich.print(f"[green]✓ Response saved to {job_dir / 'processed.txt'}[/green]")
|
||||
break
|
||||
else:
|
||||
rich.print("[bold red]Invalid choice.[/bold red]")
|
||||
|
||||
if not questionary.confirm("Transcribe another file?").ask():
|
||||
rich.print("[bold magenta]Catch you later ✌️[/bold magenta]")
|
||||
break
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
import rich
|
||||
import questionary
|
||||
except ImportError:
|
||||
print("Missing core dependencies. Please run `pip install rich questionary`.")
|
||||
sys.exit(1)
|
||||
|
||||
try:
|
||||
main()
|
||||
except KeyboardInterrupt:
|
||||
print("\n[bold magenta]Interrupted. Bye![/bold magenta]")
|
||||
19
requirements.txt
Normal file
19
requirements.txt
Normal file
@ -0,0 +1,19 @@
|
||||
# Minimal requirements for Phase 1 MVP
|
||||
# UI / tray choices are optional at this stage - keep core deps minimal
|
||||
|
||||
# For local whisper (optional when implemented)
|
||||
# faster-whisper
|
||||
|
||||
# For audio capture (choose one)
|
||||
# sounddevice
|
||||
# pyaudio
|
||||
|
||||
# OpenAI API client
|
||||
openai>=0.27.0
|
||||
|
||||
# Optional: a GUI toolkit for Phase 2
|
||||
# PySide6
|
||||
|
||||
# For packaging and utilities
|
||||
rich
|
||||
python-dotenv
|
||||
Loading…
x
Reference in New Issue
Block a user