From 032d038c5b49ae5f8372ecfbe5db29ebb618475f Mon Sep 17 00:00:00 2001 From: Mark Backman Date: Tue, 19 May 2026 18:45:52 -0400 Subject: [PATCH 1/2] Fix Windows encoding failure during project generation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `Path.write_text` / `read_text` calls in `generators/project.py` relied on the platform default encoding. On Windows that is cp1252, which cannot encode the `→` character present in cascade-mode templates (`bot_cascade.py.jinja2`, `README.md.jinja2`), causing `pipecat init quickstart` to fail mid-generation and leave a partial project directory. Pass `encoding="utf-8"` explicitly at every text I/O site, and add a regression test that simulates the Windows locale by monkeypatching the Path methods to fail when `encoding` is omitted. Fixes pipecat-ai/pipecat#4523. --- CHANGELOG.md | 9 ++++ src/pipecat_cli/generators/project.py | 22 ++++----- tests/test_project_generation.py | 67 +++++++++++++++++++++++++-- 3 files changed, 84 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c6bbd3f..d65f377 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,15 @@ All notable changes to **Pipecat** will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [Unreleased] + +### Fixed + +- `pipecat init` no longer fails on Windows when the system default + encoding is not UTF-8 (e.g. cp1252). Generated text files and template + reads in `generators/project.py` now explicitly use UTF-8, preserving + Unicode characters such as `→` that appear in cascade-mode templates. + ## [1.2.1] - 2026-05-15 ### Changed diff --git a/src/pipecat_cli/generators/project.py b/src/pipecat_cli/generators/project.py index 85ce68a..a170c16 100644 --- a/src/pipecat_cli/generators/project.py +++ b/src/pipecat_cli/generators/project.py @@ -177,12 +177,12 @@ def _generate_server_files(self, project_path: Path) -> None: # Generate server.py server_template = self.env.get_template(server_template_name) server_content = server_template.render() - (project_path / "server.py").write_text(server_content) + (project_path / "server.py").write_text(server_content, encoding="utf-8") # Generate server_utils.py utils_template = self.env.get_template(utils_template_name) utils_content = utils_template.render() - (project_path / "server_utils.py").write_text(utils_content) + (project_path / "server_utils.py").write_text(utils_content, encoding="utf-8") def _needs_aiohttp_session(self) -> bool: """Check if any selected service requires an aiohttp session.""" @@ -281,7 +281,7 @@ def _generate_bot_file(self, project_path: Path) -> None: # Render and write content = template.render(**context) bot_file = project_path / "bot.py" - bot_file.write_text(content) + bot_file.write_text(content, encoding="utf-8") def _generate_pyproject(self, project_path: Path) -> None: """Generate pyproject.toml with dependencies.""" @@ -318,7 +318,7 @@ def _generate_pyproject(self, project_path: Path) -> None: } content = template.render(**context) - (project_path / "pyproject.toml").write_text(content) + (project_path / "pyproject.toml").write_text(content, encoding="utf-8") def _generate_env_example(self, project_path: Path) -> None: """Generate .env.example with required API keys.""" @@ -337,7 +337,7 @@ def _generate_env_example(self, project_path: Path) -> None: } content = template.render(**context) - (project_path / ".env.example").write_text(content) + (project_path / ".env.example").write_text(content, encoding="utf-8") def _generate_gitignore(self, project_path: Path) -> None: """Generate .gitignore file.""" @@ -346,7 +346,7 @@ def _generate_gitignore(self, project_path: Path) -> None: "generate_client": self.config.generate_client, } content = template.render(**context) - (project_path / ".gitignore").write_text(content) + (project_path / ".gitignore").write_text(content, encoding="utf-8") def _get_service_label(self, service_value: str | None, service_list: list) -> str | None: """Get human-readable label for a service value.""" @@ -412,7 +412,7 @@ def _generate_readme(self, project_path: Path) -> None: } content = template.render(**context) - (project_path / "README.md").write_text(content) + (project_path / "README.md").write_text(content, encoding="utf-8") def _generate_dockerfile(self, project_path: Path) -> None: """Generate Dockerfile for Pipecat Cloud deployment.""" @@ -425,7 +425,7 @@ def _generate_dockerfile(self, project_path: Path) -> None: } content = template.render(**context) - (project_path / "Dockerfile").write_text(content) + (project_path / "Dockerfile").write_text(content, encoding="utf-8") def _generate_pcc_deploy(self, project_path: Path) -> None: """Generate pcc-deploy.toml for Pipecat Cloud deployment.""" @@ -437,7 +437,7 @@ def _generate_pcc_deploy(self, project_path: Path) -> None: } content = template.render(**context) - (project_path / "pcc-deploy.toml").write_text(content) + (project_path / "pcc-deploy.toml").write_text(content, encoding="utf-8") def print_next_steps(self, project_path: Path) -> None: """Print next steps for the user.""" @@ -654,7 +654,7 @@ def _render_client_template(self, template_file: Path, dest_file: Path) -> None: dest_file: Destination file path (without .jinja2) """ # Create Jinja2 environment for this specific file - template_content = template_file.read_text() + template_content = template_file.read_text(encoding="utf-8") from jinja2 import Template try: @@ -675,7 +675,7 @@ def _render_client_template(self, template_file: Path, dest_file: Path) -> None: # Render and write rendered = template.render(**context) - dest_file.write_text(rendered) + dest_file.write_text(rendered, encoding="utf-8") def _format_python_files(self, project_path: Path) -> None: """Format generated Python files with Ruff.""" diff --git a/tests/test_project_generation.py b/tests/test_project_generation.py index cabb80c..c6a0ef0 100644 --- a/tests/test_project_generation.py +++ b/tests/test_project_generation.py @@ -423,7 +423,9 @@ def test_project_generation(config_data, temp_output_dir): # Verify core files exist (in monorepo structure) assert (project_path / "server" / "bot.py").exists(), "server/bot.py should exist" - assert (project_path / "server" / "pyproject.toml").exists(), "server/pyproject.toml should exist" + assert (project_path / "server" / "pyproject.toml").exists(), ( + "server/pyproject.toml should exist" + ) assert (project_path / "server" / ".env.example").exists(), "server/.env.example should exist" assert (project_path / ".gitignore").exists(), ".gitignore should exist" assert (project_path / "README.md").exists(), "README.md should exist" @@ -511,12 +513,14 @@ def test_project_generation(config_data, temp_output_dir): elif config.video_service == "heygen_video": assert "HeyGenVideoService" in bot_content, "HeyGenVideoService should be imported" assert "heygen" in pyproject_content, "heygen extra should be in dependencies" - assert "NewSessionRequest" in bot_content, "NewSessionRequest should be imported for HeyGen" + assert "NewSessionRequest" in bot_content, ( + "NewSessionRequest should be imported for HeyGen" + ) assert "AvatarQuality" in bot_content, "AvatarQuality should be imported for HeyGen" elif config.video_service == "simli_video": assert "SimliVideoService" in bot_content, "SimliVideoService should be imported" assert "simli" in pyproject_content, "simli extra should be in dependencies" - + # Video service should be initialized in bot.py assert "video" in bot_content.lower(), "video service variable should be present" @@ -594,6 +598,63 @@ def test_project_name_conflict(temp_output_dir): # that interactively here. This test just verifies the first generation works. +def test_generation_uses_utf8_on_windows_locale(monkeypatch, temp_output_dir): + """Regression test for pipecat-ai/pipecat#4523. + + On Windows, ``Path.write_text(data)`` without an explicit ``encoding`` + falls back to the locale codec (cp1252), which cannot encode the ``→`` + characters in cascade-mode templates such as ``bot_cascade.py.jinja2`` + and ``README.md.jinja2``. This test simulates that environment by + patching ``Path.write_text`` / ``Path.read_text`` to fail when + ``encoding`` is omitted, then runs the quickstart configuration end to + end and verifies the arrow-bearing files were written intact. + """ + from pathlib import Path + + original_write = Path.write_text + original_read = Path.read_text + + def patched_write(self, data, *args, **kwargs): + if kwargs.get("encoding") is None and (len(args) == 0 or args[0] is None): + # Simulate Windows cp1252 fallback — fails on '→' (U+2192). + data.encode("cp1252") + return original_write(self, data, *args, **kwargs) + + def patched_read(self, *args, **kwargs): + if kwargs.get("encoding") is None and (len(args) == 0 or args[0] is None): + # Reading a UTF-8 template containing '→' as cp1252 raises + # UnicodeDecodeError on Windows. Force the same failure mode. + return original_read(self, *args, encoding="cp1252", **kwargs) + return original_read(self, *args, **kwargs) + + monkeypatch.setattr(Path, "write_text", patched_write) + monkeypatch.setattr(Path, "read_text", patched_read) + + # Mirror the quickstart_command config from commands/init.py. + config = ProjectConfig( + project_name="pipecat-quickstart", + bot_type="web", + transports=["smallwebrtc", "daily"], + mode="cascade", + stt_service="deepgram_stt", + llm_service="openai_responses_llm", + tts_service="cartesia_tts", + deploy_to_cloud=True, + ) + ProjectGenerator(config).generate(output_dir=temp_output_dir, non_interactive=True) + + project = temp_output_dir / "pipecat-quickstart" + bot = project / "server" / "bot.py" + readme = project / "README.md" + + assert bot.exists(), "bot.py was not written" + assert readme.exists(), "README.md was not written" + # The cascade-mode template carries '→' in a code comment; confirm it + # survived the cp1252-simulating environment. + assert "→" in bot.read_text(encoding="utf-8") + assert "→" in readme.read_text(encoding="utf-8") + + def test_invalid_service_combination(): """Test that invalid service combinations are caught.""" # This test would check validation logic if we add it From 7d43f3292a1d0e37068b43e555fabf9527e86354 Mon Sep 17 00:00:00 2001 From: Mark Backman Date: Tue, 19 May 2026 18:48:08 -0400 Subject: [PATCH 2/2] Bump version to 1.2.2 --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d65f377..4e205c6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ All notable changes to **Pipecat** will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [Unreleased] +## [1.2.2] - 2026-05-19 ### Fixed