Dzisiaj jest jakieś święto, i jest dzień wolny od pracy, więc więc zamiast programować dla prywaciarza za psie pieniądze, programuję dla zabawy za darmo. A jest z czego się cieszyć! Pierwsze dzieło to
Skrypt do wgrywania tej właśnie strony
Od teraz zamiast:
- pisać w Terminalu komendę do budowania strony
zola build
- odpalać graficznego klienta FTP (Cyberduck)
- otwierać folder, do którego zbudowała się strona
- zaznaczać i przeciągać pliki strony do klienta FTP
- potwierdzać że tak, chcę nadpisać wszystko
- czekać, aż pasek postępu się wypełni, po to, żeby zamknąć klienta FTP
wystarczy w Terminalu wpisać ✨✨✨ ./deplojuj.sh
✨✨✨ i się deplojuje.
#!/bin/bash
REMOTE_USER="user"
REMOTE_HOST="ftp.domain.com"
REMOTE_PATH="blog"
LOCAL_BUILD_DIR="public"
# Check if .netrc exists
if [ ! -f ~/.netrc ]; then
echo "Error: ~/.netrc file not found!"
echo "Please create ~/.netrc with the following format:"
echo "machine $REMOTE_HOST login $REMOTE_USER password YOUR_PASSWORD"
echo "Make sure to set proper permissions: chmod 600 ~/.netrc"
exit 1
fi
# Build the site
echo "Building the site..."
zola build --output-dir $LOCAL_BUILD_DIR
# Check if local build directory exists
if [ ! -d "$LOCAL_BUILD_DIR" ]; then
echo "Error: Local build directory '$LOCAL_BUILD_DIR' not found!"
exit 1
fi
echo "Deploying from '$LOCAL_BUILD_DIR' to '$REMOTE_HOST:$REMOTE_PATH'..."
# Use lftp to sync files
lftp << EOF
open sftp://$REMOTE_USER@$REMOTE_HOST
mirror -R --delete $LOCAL_BUILD_DIR $REMOTE_PATH
quit
EOF
if [ $? -eq 0 ]; then
echo "Deployment completed successfully!"
else
echo "Deployment failed!"
exit 1
fi
Oczywiście nie jest to nic wielkiego, ale dawno temu usłyszałem gdzieś (chyba od CGP Grey'a), że takie najmniejsze optymalizacje mają największy wpływ na samopoczucie. To szło jakoś tak, że człowiek nie rejestruje ich na świadomym poziomie, ale podskórnie nadal to "irytuje" układ nerwowy, i zredukowanie tego "swędzenia", chociaż tak samo niekoniecznie rejestrowalne na poziomie świadomości, poprawia samopoczucie, sprawia, że ptaki piękniej śpiewają, chmury są mniej szare, a ludzie mniej irytujący. I nawet jeśli sobie to wszystko wymyśliłem i wmówiłem, to i tak stwierdzam, że ptaki faktycznie piękniej śpiewają odkąd napisałem ten skrypt.
Zola dogaduje się z Obsidianem
Udało mi się też ogarnąć kwestię wyświetlania obrazków i generalnie assetów zarówno w Zoli, jak i w Obsidianie. Do tej pory wszystko obrazki czy fragmenty kodu umieszczałem w osobnym folderze o nazwie assets
, i żeby obrazki poprawnie wyświetlały się na stronie, ich ścieżka musiała być dosłownie kosmiczna, coś w stylu ../../assets/images/kotek.png
. Tego koszmaru oczywiście nie mógł znieść Obsidian, więc wyświetlał błąd zamiast obrazka. A ponieważ te posty piszę często w Obsidianie, to musiałem to naprawić. No i się udało. Zola wspiera coś, co nazywa się asset colocation, czyli że można sobie wszystkie pliki towarzyszące stronie wrzucić do tego samego fodleru, i podawać po prostu lokalnie ścieżki typu 
. No działa ślicznie.
Mogłem przeczytać w 15 minut, ale napisałem program w 8 godzin
Większość dzisiejszego dnia spędziłem na tworzeniu skryptu w Pythonie, który dowolny plik Markdown (to są jakieś inne?)... syntezuje? W sensie robi syntezę mowy. Zaczęło się od tego, że był jakiś strasznie długi artykuł, którego nie chciało mi się czytać. Przypomniałem sobie, że jest coś takiego jak Whisper, który robi tekst z nagrań, ale może w drugą stronę też działa? Trochę grzebania po internecie, i znalazłem najpierw ten wątek na Reddicie, potem inny wątek na Reddicie, potem to repozytorium na GitHubie i zainspirowany tym wszystkim, napisałem swoją wariację na temat przetwarzania tekstu na mowę.
Kilka ciekawostek dla znawców Pythona (którym sam raczej nie jestem). Pierwsza sprawa to to, ze jakieś 20% czasu spędziłem na walce z typami, bo jako programista statycznie typowanego Swifta, moja dusza cierpiała od samego patrzenia na kod bez adnotacji typów. Ostatecznie poległem. To była uczciwa walka i zostałem pokonany. Druga sprawa to to, że skrypt do działania wymaga instalacji zależności, a jak zależności, to wiadomo, że venv
. No i pierwsza edycja tego mojego pisania składała się ze skryptu bash
, który tego venv
a zestawiał, instalował zależności i odpalał co trzeba. Ale okazuje się, że Python - czy może precyzyjniej skrypt w Pythonie - może sobie swoje środowisko sam stworzyć, zainstalować zależności, a potem się w nim uruchomić! Ja wiem, że to jest komputer, i on robi to, co programista nakazał, ale muszę się przespać z tą rewelacją.
Anyway, moje dzieło można podziwiać poniżej.
#!/usr/bin/env python3.12
import argparse
import os
import re
import subprocess
import sys
import venv
import warnings
from datetime import datetime
from pathlib import Path
AUDIO_EXT = '.wav'
DEFAULT_VOICE = 'af_heart'
VENV_DIR = Path(__file__).parent / ".kokoro-venv"
PACKAGES = ["kokoro>=0.9.4", "soundfile"]
"""
CLI tool for offline text-to-speech synthesis using Kokoro-82M.
Usage: python3.12 tts.py -f example.md -v af_nicole
"""
pipeline = None
def ensure_venv():
if sys.prefix != sys.base_prefix:
return
if not VENV_DIR.exists():
print("📦 Setting up virtual environment...")
venv.create(VENV_DIR, with_pip=True)
bin_dir = "Scripts" if os.name == "nt" else "bin"
pip_exe = VENV_DIR / bin_dir / "pip"
subprocess.check_call([str(pip_exe), "install", *PACKAGES])
print("🔄 Switching to virtual environment...")
python_exe = VENV_DIR / ("Scripts" if os.name == "nt" else "bin") / "python"
result = subprocess.run([str(python_exe)] + sys.argv)
sys.exit(result.returncode)
def get_pipeline():
global pipeline
if pipeline is not None:
return pipeline
ensure_venv()
print("🤖 Initializing TTS pipeline...")
with warnings.catch_warnings():
warnings.simplefilter("ignore")
from kokoro import KPipeline # type: ignore
pipeline = KPipeline(lang_code='a', repo_id='hexgrad/Kokoro-82M')
return pipeline
def text_to_audio(text, voice, speed, output_path=None):
print("🎯 Converting text to audio...")
# Generate audio segments
segments = [s.strip() for s in re.split(r'\n+', text.strip()) if s.strip()]
generator = get_pipeline()(text, voice=voice, speed=speed, split_pattern=r'\n+')
audio_data = []
total_segments = len(segments)
for i, (_, _, audio) in enumerate(generator):
current = i + 1
percent = int(current / total_segments * 100)
print(f"\r⏳ Processing: {percent}% ({current}/{total_segments})", end='', flush=True)
audio_data.extend(audio)
print()
# Save audio
if output_path is None:
output_path = datetime.now().strftime('%y-%m-%d-%H-%M-%S' + AUDIO_EXT)
elif not output_path.endswith(AUDIO_EXT):
output_path += AUDIO_EXT
print("💾 Writing audio file...")
import soundfile # type: ignore
soundfile.write(output_path, audio_data, 24000)
return output_path
def process_file(filepath, voice, speed, output_path=None):
print(f"📄 Processing: {filepath}")
with open(filepath, 'r') as f:
text = f.read()
if output_path is None:
output_path = f"{Path(filepath).stem}{AUDIO_EXT}"
return text_to_audio(text, voice, speed, output_path)
def process_batch(voice, speed):
print("📚 Batch processing...")
for md_file in Path('.').glob('*.md'):
wav_file = md_file.with_suffix(AUDIO_EXT)
if wav_file.exists():
print(f"⏭️ Skipping {md_file} (already exists)")
continue
process_file(str(md_file), voice, speed)
def main():
parser = argparse.ArgumentParser(description="CLI tool for offline text-to-speech synthesis.")
parser.add_argument('text', nargs='?', help="Text to synthesize")
parser.add_argument('-f', '--file', help="File to process")
parser.add_argument('-v', '--voice', default=DEFAULT_VOICE, help=f"Voice (default: {DEFAULT_VOICE})")
parser.add_argument('-s', '--speed', type=float, default=1.0, help="Speech speed")
parser.add_argument('-o', '--output', help="Output filename")
parser.add_argument('--batch', action='store_true', help="Process all .md files")
args = parser.parse_args()
if not (args.text or args.file or args.batch):
parser.print_help()
sys.exit(1)
print("🚀 Starting TTS...")
if args.batch:
process_batch(args.voice, args.speed)
elif args.file:
process_file(args.file, args.voice, args.speed, args.output)
elif args.text:
text_to_audio(args.text, args.voice, args.speed, args.output)
print("🎉 Done!")
if __name__ == "__main__":
main()
Klasycznie, do programowania został użyty Cursor, model Claude 4 Sonnet, oraz następująca piosenka