commit 3d08f95b17bb30f93993d5bc10638e1846b61799 Author: ru4ls <126259521+ru4ls@users.noreply.github.com> Date: Fri Oct 17 16:49:30 2025 +0700 Initial commit diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..990d586 --- /dev/null +++ b/.env.example @@ -0,0 +1 @@ +GOOGLE_STREET_VIEW_API_KEY = "PASTE_YOUR_API_KEY_HERE" \ No newline at end of file diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..dfe0770 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# Auto detect text files and perform LF normalization +* text=auto diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e6f03b1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,53 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg + +# Logs +*.log + +# Environment +env/ +venv/ +ENV/ +.venv/ +env.bak/ +venv.bak/ +.env + +# IDE +.idea/ +.vscode/ +*.swp +*.swo + +# ComfyUI +*.bin +*.pth +output/ +input/ + +# Config +config.json + +# OS +.DS_Store +Thumbs.db \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..9413a2c --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 ru4ls + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..55d811c --- /dev/null +++ b/README.md @@ -0,0 +1,163 @@ +# ComfyUI Street View Loader Node + +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) +[![Python 3.12](https://img.shields.io/badge/Python-3.12-blue.svg)](https://www.python.org/downloads/release/python-3120/) + + +A custom node for ComfyUI that allows you to load images directly from Google Street View to use as backgrounds, textures, or inputs in your workflows. + +Instead of manually taking screenshots, this node programmatically fetches a clean, high-resolution image from any location on Earth with Street View coverage, giving you precise control over the camera angle, direction, and field of view. + +![Street View Loader Node combine with NanoBanana in ComfyUI](media/preview.png) + +--- + +## Why Use This Node? The Power of "Ground Truth" + +While standard text-to-image models are incredible at *imagining* locations ("a charming street in Paris"), they struggle to recreate the **exact appearance and layout** of a specific, real-world place. Their output is an approximation, an artistic blend of countless images. + +This node solves that problem by providing **photographic ground truth**. It pulls the real, pixel-for-pixel view of a location, ensuring your generations are anchored in reality. + +This gives you the best of both worlds: the authenticity of a real photograph combined with the creative power of AI. + +### Key Advantages & Use Cases + +* **Guaranteed Location Accuracy:** When you need the background to be recognizably *your* street, a specific landmark, or a favorite travel spot, this node is the only way to guarantee a 100% accurate representation. The composition, architecture, and lighting are real, not AI-generated guesswork. + +* **The Ultimate Base for `img2img` and ControlNet:** This is the most powerful use case. You can feed the real Street View image into your workflow as a base for: + * **Stylization:** Keep the exact composition of a place while transforming it into an oil painting, an anime scene, or a charcoal sketch. + * **Restoration/Re-imagining:** Take a view of an old neighborhood and use AI to add futuristic elements, or render it as it might have looked in a different historical period. + * **Precise Inpainting:** Add characters, objects, or fantastical elements into a real-world scene with a background that is perfectly stable and coherent. + +* **Virtual Location Scouting:** For concept artists, filmmakers, and game developers. You can instantly scout real-world locations from your desktop, pull them into ComfyUI, and experiment with different styles and moods for your project without ever leaving your chair. + +* **Personalized and Sentimental Art:** Create a unique piece of art based on a place with personal meaning—a childhood home, a proposal spot, or a favorite vacation view. This creates a connection that a generic prompt could never achieve. + +* **Consistent Backgrounds for Testing:** When testing LoRAs, IPAdapters, or character models, using a consistent, real-world background from this node ensures that you are only seeing the changes from your model, not the random variations of a `txt2img` background. + +![Street View Loader Node combine with NanoBanana in ComfyUI](media/preview_2.png) + +## Features +- **Direct API Integration:** Pulls images directly from the Google Street View Static API. +- **Full Camera Control:** Adjust Location, Heading (pan), Pitch (tilt), and Field of View (zoom). +- **Secure API Key Storage:** Uses a `.env` file to keep your API key safe and out of your workflow files. +- **Clean Output:** No UI overlays, just the pure image. +- **Easy Workflow Integration:** Outputs a standard `IMAGE` tensor and a metadata string (the fetch URL) for debugging. + +--- + +## 1. Installation + +1. Navigate to your ComfyUI `custom_nodes` directory: + ```bash + cd ComfyUI/custom_nodes/ + ``` + +2. Clone this repository: + ```bash + git clone https://github.com/ru4ls/ComfyUI_StreetView-Loader.git + ``` + +3. Install the required Python dependencies. Open a terminal, navigate to the new `ComfyUI_StreetView-Loader` folder, and run: + ```bash + pip install -r requirements.txt + ``` + * this will install the `requests` and `python-dotenv` libraries, which are required for this node.* + +4. **Restart ComfyUI.** + +--- + +## 2. Setup: Getting Your Google API Key (Crucial Step) + +This node requires a Google Cloud API key to function. Google provides a generous **$200/month free credit**, which is more than enough for extensive personal use without any charge. + +### Step-by-Step Guide + +**Part A: Create Project & Enable API** +1. Go to the [Google Cloud Console](https://console.cloud.google.com/). +2. If you don't have a project, create one. If you do, click the project selector at the top and click **"New Project"**. Give it a name like `ComfyUI-API`. +3. Once the project is created, search for "API Library" in the top search bar. +4. In the library, search for and select **"Street View Static API"**. +5. Click the **"Enable"** button. + +**Part B: Set Up Billing** +6. You will be prompted to link a billing account. This is required, but you will **not be charged** unless you exceed the $200 free monthly credit (equivalent to tens of thousands of image requests). + +**Part C: Create and Secure Your API Key** +7. In the Cloud Console search bar, navigate to **"Credentials"**. +8. Click **"+ Create Credentials"** and select **"API key"**. +9. Your new API key will be displayed. **Copy this key immediately.** +10. **(IMPORTANT!)** Click the **"Edit API key"** button. Under "API restrictions," select **"Restrict key"**. +11. In the dropdown, find and check **"Street View Static API"**, then click "OK". This ensures your key can *only* be used for this purpose, protecting your account. Click **"Save"**. + +**Part D: Configure the Node** +12. In your `ComfyUI/custom_nodes/ComfyUI_StreetView-Loader/` folder, create a new file named `.env`. or rename an existing `.env.example` file to `.env`. +13. Open this `.env` file and add your copied API key in the following format: + ``` + GOOGLE_STREET_VIEW_API_KEY="your_actual_api_key_goes_here" + ``` +14. Save the file. + +Your setup is now complete! + +--- + +## 3. How to Use + +1. After restarting ComfyUI, double-click the canvas and search for **"Street View Loader"**. +2. The node will appear with several input fields. + +### Finding Your Parameters + +The best way to get the exact `location`, `heading`, `pitch`, and `fov` is to use the Google Maps URL. + +1. Open [Google Maps](https://maps.google.com) and drop into Street View at your desired location. +2. Pan, tilt, and zoom the camera until you frame the perfect shot. +3. Look at the URL in your browser's address bar. It will look like this: + `https://www.google.com/maps/@**40.74844,-73.98566**,3a,**75**y,**273.99**h,**85.73**t/data=...` + + - `@**40.74844,-73.98566**`: This is the **`location`**. + - `**75**y`: This is the **`fov`** (Field of View / Zoom). + - `**273.99**h`: This is the **`heading`**. + - `**85.73**t`: This is related to the **`pitch`**. The API uses a range of -90 (straight down) to +90 (straight up) with 0 being the horizon. A `t` value of `90` in the URL is roughly a `pitch` of `0` for the API. Adjust as needed. + +4. Copy these values into the corresponding fields on the node and generate! + +### **The Easy Way: Using the URL Parser Node** + +As of version 2.0, the recommended workflow is to use the **Street View URL Parser** node. This node extracts all the camera parameters automatically from a single Google Maps URL. + +1. Add two nodes to your workflow: + - `Street View URL Parser` (found in `Ru4ls/StreetView/Utils`) + - `Street View Loader` + +2. Find the exact view you want in Google Street View on your browser. + +3. Copy the **entire URL** from your browser's address bar. + +4. Paste the URL into the `url` field of the **Parser** node. + +5. Connect the outputs of the Parser node to the inputs of the Loader node (`location` to `location`, `heading` to `heading`, etc.). + + +This method is faster, easier, and less error-prone than manually entering each parameter. + +### Node Inputs +- `location`: The latitude and longitude (e.g., `40.74844,-73.98566`). +- `heading`: The compass direction [0 to 360]. 0 is North, 90 is East, 180 is South, 270 is West. +- `pitch`: The up/down tilt of the camera [-90 to 90]. 0 is the horizon. +- `fov`: The field of view, or zoom [10 to 120]. A lower number is more zoomed in. +- `width` / `height`: The dimensions of the output image (max 640x640 for the free API). + +--- + +## 4. Troubleshooting + +- **Node throws a `ValueError: API key not found`:** Your `.env` file is missing, in the wrong location, or the variable name is not `GOOGLE_STREET_VIEW_API_KEY`. It must be in the `ComfyUI_StreetView-Loader` folder. +- **The node outputs a black image:** This almost always means Google has no Street View imagery for that exact coordinate, or the API key is invalid/restricted. Check your key's restrictions in the Google Cloud Console and try a slightly different coordinate. The `metadata` output will contain an error message. +- **The node doesn't appear in ComfyUI:** Ensure you have fully restarted the ComfyUI server after installation. + +## License + +This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. \ No newline at end of file diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..003661e --- /dev/null +++ b/__init__.py @@ -0,0 +1,20 @@ +# file: ComfyUI_StreetView-Loader\__init__.py + +from .nodes.streetview_loader import StreetViewLoader +from .nodes.streetview_url_parser import StreetViewURLParser + +NODE_CLASS_MAPPINGS = { + "StreetViewLoader": StreetViewLoader, + "StreetViewURLParser": StreetViewURLParser, +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "StreetViewLoader": "Street View Loader", + "StreetViewURLParser": "Street View URL Parser", +} + +__all__ = ['NODE_CLASS_MAPPINGS', 'NODE_DISPLAY_NAME_MAPPINGS'] + +print("------------------------------------------") +print("ComfyUI Street View Loader Node: Loaded.") +print("------------------------------------------") \ No newline at end of file diff --git a/media/preview.png b/media/preview.png new file mode 100644 index 0000000..4b961d0 Binary files /dev/null and b/media/preview.png differ diff --git a/media/preview_2.png b/media/preview_2.png new file mode 100644 index 0000000..67f2ca9 Binary files /dev/null and b/media/preview_2.png differ diff --git a/nodes/__init__.py b/nodes/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/nodes/streetview_loader.py b/nodes/streetview_loader.py new file mode 100644 index 0000000..e626e61 --- /dev/null +++ b/nodes/streetview_loader.py @@ -0,0 +1,77 @@ +# file: ComfyUI_StreetView-Loader\nodes\streetview_loader.py + +import torch +import numpy as np +import os +from dotenv import load_dotenv +from PIL import Image + +from ..utils.connect_api_utils import fetch_streetview_image + +# --- Load API Key from .env file --- +current_dir = os.path.dirname(os.path.abspath(__file__)) +parent_dir = os.path.dirname(current_dir) +dotenv_path = os.path.join(parent_dir, '.env') + +# Load the environment variables from the found .env file +load_dotenv(dotenv_path=dotenv_path) + +# Get the API key from the loaded environment variables +API_KEY_FROM_ENV = os.getenv("GOOGLE_STREET_VIEW_API_KEY") + + +class StreetViewLoader: + """ + A ComfyUI node to load images directly from the Google Street View Static API, + using a secret API key stored in a .env file. + """ + + @classmethod + def INPUT_TYPES(s): + """ + Defines the input fields for the node. The API key is handled in the + background and is not exposed as an input in the UI. + """ + return { + "required": { + "location": ("STRING", { + "multiline": False, + "default": "40.720032,-73.988354" # Example: Near Katz's Deli, NYC + }), + "heading": ("FLOAT", {"default": 151.78, "min": 0, "max": 360, "step": 0.1, "display": "slider"}), + "pitch": ("FLOAT", {"default": -0.76, "min": -90, "max": 90, "step": 0.1, "display": "slider"}), + "fov": ("INT", {"default": 90, "min": 10, "max": 120, "step": 1, "display": "slider"}), + "width": ("INT", {"default": 512, "min": 64, "max": 640, "step": 8}), + "height": ("INT", {"default": 512, "min": 64, "max": 640, "step": 8}), + } + } + + RETURN_TYPES = ("IMAGE", "STRING") + RETURN_NAMES = ("image", "metadata") + FUNCTION = "load_image" + CATEGORY = "Ru4ls/StreetView" + + def load_image(self, location, heading, pitch, fov, width, height): + + if not API_KEY_FROM_ENV: + raise ValueError("Google Street View API key not found in .env file. Please ensure GOOGLE_STREET_VIEW_API_KEY is set in ComfyUI_StreetView-Loader/.env") + + image_pil, metadata = fetch_streetview_image( + api_key=API_KEY_FROM_ENV, + location=location, + heading=heading, + pitch=pitch, + fov=fov, + width=width, + height=height + ) + + # Convert the returned PIL image to the tensor format ComfyUI expects. + image_tensor = self.pil_to_tensor(image_pil) + + return (image_tensor, metadata) + + def pil_to_tensor(self, image: Image.Image): + """ Helper function to convert a PIL Image to a PyTorch Tensor for ComfyUI. """ + image_np = np.array(image).astype(np.float32) / 255.0 + return torch.from_numpy(image_np)[None,] \ No newline at end of file diff --git a/nodes/streetview_url_parser.py b/nodes/streetview_url_parser.py new file mode 100644 index 0000000..a8f0bb8 --- /dev/null +++ b/nodes/streetview_url_parser.py @@ -0,0 +1,65 @@ +import re + +class StreetViewURLParser: + """ + A ComfyUI node that takes a Google Maps Street View URL and parses it + into its constituent parts: location, heading, pitch, and fov. + """ + + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "url": ("STRING", { + "multiline": True, + "default": "Paste a Google Maps Street View URL here" + }), + } + } + + RETURN_TYPES = ("STRING", "FLOAT", "FLOAT", "INT") + RETURN_NAMES = ("location", "heading", "pitch", "fov") + FUNCTION = "parse_url" + CATEGORY = "Ru4ls/StreetView/Utils" # A sub-category for utilities + + def parse_url(self, url): + # Default values in case parsing fails + location = "40.720032,-73.988354" + heading = 151.78 + pitch = -0.76 + fov = 90 + + if not url or "google.com/maps" not in url: + print("Invalid URL provided. Using default values.") + return (location, heading, pitch, fov) + + # --- Use Regular Expressions to find the parameters --- + + # 1. Parse Location (latitude,longitude) + # Looks for the @ symbol followed by two numbers separated by a comma + loc_match = re.search(r"@(-?\d+\.\d+),(-?\d+\.\d+)", url) + if loc_match: + lat = loc_match.group(1) + lon = loc_match.group(2) + location = f"{lat},{lon}" + + # 2. Parse Heading, Pitch, and FOV + # Looks for the data block like ",3a,75y,273.99h,85.73t" + params_match = re.search(r",(\d+\.?\d*)y,(\d+\.?\d*)h,(\d+\.?\d*)t", url) + if params_match: + # FOV is the number before 'y' + fov = int(float(params_match.group(1))) + + # Heading is the number before 'h' + heading = float(params_match.group(2)) + + # Pitch from URL needs conversion for the API + # URL pitch (t): 90 is horizontal. + # API pitch: 0 is horizontal. + # Conversion: API_pitch = URL_t - 90 + url_pitch = float(params_match.group(3)) + pitch = url_pitch - 90.0 + + print(f"Parsed URL: Location={location}, Heading={heading}, Pitch={pitch}, FOV={fov}") + + return (location, heading, pitch, fov) \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..d44fe44 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +requests +python-dotenv \ No newline at end of file diff --git a/utils/connect_api_utils.py b/utils/connect_api_utils.py new file mode 100644 index 0000000..8248776 --- /dev/null +++ b/utils/connect_api_utils.py @@ -0,0 +1,51 @@ +# file: ComfyUI_StreetView-Loader\utils\connect_api_utils.py + +import requests +from PIL import Image +from io import BytesIO + +def fetch_streetview_image(api_key, location, heading, pitch, fov, width, height): + """ + Connects to the Google Street View API and fetches an image. + + Returns: + A tuple containing (PIL.Image, metadata_url_string) on success, + or (error_image, error_message_string) on failure. + """ + base_url = "https://maps.googleapis.com/maps/api/streetview" + + params = { + "size": f"{width}x{height}", + "location": location, + "heading": heading, + "pitch": pitch, + "fov": fov, + "key": api_key, + "return_error_codes": "true" + } + + try: + response = requests.get(base_url, params=params, timeout=20) + response.raise_for_status() + + # Check for "ZERO_RESULTS" or other API errors which still return a 200 OK status. + # A valid JPEG starts with bytes FF D8. A valid PNG starts with 89 50 4E 47. + is_valid_image = response.content.startswith(b'\xff\xd8') or response.content.startswith(b'\x89PNG') + + if not is_valid_image: + error_message = "API returned no image for this location. It might not be available." + print(f"StreetView Info: {error_message}") + error_img = Image.new('RGB', (width, height), color='black') + return (error_img, error_message) + + # Success case + image_pil = Image.open(BytesIO(response.content)).convert("RGB") + metadata_url = requests.Request('GET', base_url, params=params).prepare().url + print(f"StreetView URL: {metadata_url}") + return (image_pil, metadata_url) + + except requests.exceptions.RequestException as e: + error_message = f"An API request error occurred: {e}" + print(error_message) + error_img = Image.new('RGB', (width, height), color='black') + return (error_img, error_message) \ No newline at end of file