Example #1
0
    def get_image(self, record: ModelRecord):
        o_id = Controller.get_unique_id()
        self.communicate({"$type": "add_object",
                          "name": record.name,
                          "url": record.get_url(),
                          "scale_factor": record.scale_factor,
                          "rotation": record.canonical_rotation,
                          "id": o_id})

        s = TDWUtils.get_unit_scale(record) * 2

        # Scale the model and get an image.
        # Look at the model's centroid.
        resp = self.communicate([{"$type": "scale_object",
                                  "id": o_id,
                                  "scale_factor": {"x": s, "y": s, "z": s}},
                                 {"$type": "look_at",
                                  "avatar_id": "a",
                                  "object_id": o_id,
                                  "use_centroid": True}])
        # Destroy the model and unload the asset bundle.
        self.communicate([{"$type": "destroy_object",
                          "id": o_id},
                          {"$type": "unload_asset_bundles"}])
        return Images(resp[0]), resp[-1]
Example #2
0
    def add_transforms_object(self,
                              record: ModelRecord,
                              position: Dict[str, float],
                              rotation: Dict[str, float],
                              o_id: Optional[int] = None) -> dict:
        """
        This is a wrapper for `Controller.get_add_object()` and the `add_object` command.
        This caches the ID of the object so that it can be easily cleaned up later.

        :param record: The model record.
        :param position: The initial position of the object.
        :param rotation: The initial rotation of the object, in Euler angles.
        :param o_id: The unique ID of the object. If None, a random ID is generated.

        :return: An `add_object` command.
        """

        if o_id is None:
            o_id: int = Controller.get_unique_id()

        # Log the static data.
        self.object_ids = np.append(self.object_ids, o_id)

        return {
            "$type": "add_object",
            "name": record.name,
            "url": record.get_url(),
            "scale_factor": record.scale_factor,
            "position": position,
            "rotation": rotation,
            "category": record.wcategory,
            "id": o_id
        }
Example #3
0
    def __init__(self,
                 record_path: str,
                 asset_bundle_path: str,
                 build_path: str,
                 port=1071):
        """
        :param record_path: The path to the temporary record file.
        :param asset_bundle_path: The path to the local asset bundle.
        :param build_path: The path to the build executable.
        :param port: The port.
        """

        self.record_path: str = record_path
        self.record: ModelRecord = ModelRecord(
            json.loads(Path(record_path).read_text(encoding="utf-8")))
        self.build_path: str = build_path
        self.asset_bundle_path: str = asset_bundle_path
        if not self.asset_bundle_path.startswith("file:///"):
            self.asset_bundle_path = "file:///" + self.asset_bundle_path

        try:
            # Create the build.
            Popen(build_path)
        except:
            print("No build found at: " + build_path)
            print("You need to launch a build manually.")

        super().__init__(port)
Example #4
0
    def create_library(self) -> ModelLibrarian:
        lib = self._get_librarian("ShapeNetSEM")
        first_time_only = True
        metadata_path = self.src.joinpath("metadata.csv")
        with open(str(metadata_path), newline='') as csvfile:
            reader = csv.reader(csvfile)
            for row in reader:
                if first_time_only:
                    first_time_only = False
                    continue
                if row[1] == "" or row[3] == "":
                    continue
                record = ModelRecord()
                record.name = row[0][4:]
                record.wcategory = row[3].split(",")[0]
                record.wnid = f"n{int(row[2][1:]):08d}"

                for platform in record.urls:
                    record.urls[platform] = self._get_url(
                        record.wnid, record.name, platform)
                lib.add_or_update_record(record, overwrite=False, write=False)
        # Write to disk.
        lib.write(pretty=False)

        # Move the textures.
        any_textures = False
        for f in self.src.joinpath("textures").rglob("*.jpg"):
            f.replace(self.src.joinpath(f"models/{f.name}"))
            any_textures = True
        if any_textures:
            print("Moved all .jpg files in textures/ to models/")
        return lib
Example #5
0
    def __init__(self, record_path: str, asset_bundle_path: str, port=1071):
        """
        :param record_path: The path to the temporary record file.
        :param asset_bundle_path: The path to the local asset bundle.
        :param port: The port.
        """

        self.record_path: str = record_path
        self.record: ModelRecord = ModelRecord(
            json.loads(Path(record_path).read_text(encoding="utf-8")))
        self.asset_bundle_path: str = asset_bundle_path
        if not self.asset_bundle_path.startswith("file:///"):
            self.asset_bundle_path = "file:///" + self.asset_bundle_path

        super().__init__(port)
Example #6
0
    def create_library(self) -> ModelLibrarian:
        # Load the taxonomy file.
        metadata = json.loads(
            self._get_metadata_path().read_text(encoding="utf-8"))
        # Create a new library.
        lib = self._get_librarian("ShapeNetCoreVal")

        # Process each .obj file.
        for f in self.src.rglob("*.obj"):
            record = ModelRecord()
            record.name = f.parts[-3]
            record.wnid = metadata[record.name]['wnid']
            record.wcategory = metadata[record.name]['wcategory']
            for platform in record.urls:
                record.urls[platform] = self._get_url(record.wnid, record.name,
                                                      platform)

            lib.add_or_update_record(record, overwrite=False, write=False)

        # Write to disk. Don't pretty-print (saves about 60 MB).
        lib.write(pretty=False)
        return lib
Example #7
0
    def create_library(self) -> ModelLibrarian:
        # Load the taxonomy file.
        taxonomy_raw = json.loads(self._get_taxonomy_path().read_text(encoding="utf-8"))
        taxonomy = dict()
        for synset in taxonomy_raw:
            taxonomy.update({synset["synsetId"]: synset["name"].split(",")[0]})
        # Create a new library.
        lib = self._get_librarian("ShapeNetCore")

        # Process each .obj file.
        for f in self.src.rglob("*.obj"):
            wnid = f.parts[-4]
            record = ModelRecord()
            record.name = f.parts[-3]
            record.wnid = "n" + wnid
            record.wcategory = taxonomy[wnid]
            for platform in record.urls:
                record.urls[platform] = self._get_url(record.wnid, record.name, platform)

            lib.add_or_update_record(record, overwrite=False, write=False)

        # Write to disk. Don't pretty-print (saves about 60 MB).
        lib.write(pretty=False)
        return lib
Example #8
0
    def create_record(self,
                      model_name: str,
                      wnid: int,
                      wcategory: str,
                      scale: float,
                      urls: Dict[str, str],
                      record: Optional[ModelRecord] = None,
                      write_physics: bool = False) -> Path:
        """
        Create a local .json metadata record of the model.

        :param model_name: The name of the model.
        :param wnid: The WordNet ID.
        :param wcategory: The WordNet category.
        :param scale: The default scale of the object.
        :param urls: The finalized URLs (or local filepaths) of the assset bundles.
        :param record: A pre-written metadata record. If not None, it will override the other parameters.
        :param write_physics: If true, launch the build to write the physics quality. (This is optional).

        :return The path to the file with the metadata record.
        """

        # Write the record.
        if not self.quiet:
            print("Creating a record.")

        if record is None:
            record = ModelRecord()
            record.name = model_name
            record.wnid = f'n{wnid:08d}'
            record.wcategory = wcategory
            record.urls = urls
            record.scale = scale

        # Append asset bundle sizes.
        local_path = Path.home().joinpath(
            "asset_bundle_creator/Assets/NewAssetBundles").joinpath(
                record.name)
        for os_dir in local_path.iterdir():
            if not os_dir.is_dir():
                continue
            asset_bundle_platform = UNITY_TO_SYSTEM[os_dir.stem]
            size = os_dir.joinpath(record.name).stat().st_size
            record.asset_bundle_sizes[asset_bundle_platform] = size

        # Assemble a dictionary of just the data that we don't need the Editor for.
        record_data = {
            "name": record.name,
            "urls": record.urls,
            "wnid": record.wnid,
            "wcategory": record.wcategory,
            "scale_factor": record.scale_factor,
            "do_not_use": record.do_not_use,
            "do_not_use_reason": record.do_not_use_reason,
            "canonical_rotation": record.canonical_rotation,
            "physics_quality": -1,
            "asset_bundle_sizes": record.asset_bundle_sizes
        }

        # Serialize the record.
        record_data = json.dumps(record_data)
        # Remove the last } and replace it with , to keep serializing with Unity.
        record_data = record_data[:-1] + ","

        record_path = self.get_assets_directory().joinpath(model_name +
                                                           ".json")
        record_path.write_text(record_data, encoding="utf-8")

        record_call = self.unity_call[:]
        record_call.extend([
            "-executeMethod", "RecordCreator.WriteRecord",
            "-modelname=" + model_name, "-scale=" + str(scale)
        ])
        call(record_call)

        # Test the record.
        try:
            json.loads(record_path.read_text(encoding="utf-8"))
        except json.JSONDecodeError:
            raise Exception("Failed to deserialize: " +
                            record_path.read_text(encoding="utf-8"))

        if not self.quiet:
            print("Wrote the record data to the disk.")

        if write_physics:
            self.write_physics_quality(
                record_path=record_path,
                asset_bundle_path=self.get_local_asset_bundle_path(model_name))

        return record_path
Example #9
0
    def trial(self, scene: Scene, record: ModelRecord, output_path: Path,
              scene_index: int) -> None:
        """
        Run a trial in a scene that has been initialized.

        :param scene: Data for the current scene.
        :param record: The model's metadata record.
        :param output_path: Write the .wav file to this path.
        :param scene_index: The scene identifier.
        """

        self.py_impact.reset(initial_amp=0.05)

        # Initialize the scene, positioning objects, furniture, etc.
        resp = self.communicate(scene.initialize_scene(self))
        center = scene.get_center(self)

        max_y = scene.get_max_y()

        # The object's initial position.
        o_x = RNG.uniform(center["x"] - 0.15, center["x"] + 0.15)
        o_y = RNG.uniform(max_y - 0.5, max_y)
        o_z = RNG.uniform(center["z"] - 0.15, center["z"] + 0.15)
        # Physics values.
        mass = self.object_info[record.name].mass + RNG.uniform(
            self.object_info[record.name].mass * -0.15,
            self.object_info[record.name].mass * 0.15)
        static_friction = RNG.uniform(0.1, 0.3)
        dynamic_friction = RNG.uniform(0.7, 0.9)
        # Angles of rotation.
        yaw = RNG.uniform(-30, 30)
        pitch = RNG.uniform(0, 45)
        roll = RNG.uniform(-45, 45)
        # The force applied to the object.
        force = RNG.uniform(0, 5)
        # The avatar's position.
        a_r = RNG.uniform(1.5, 2.2)
        a_x = center["x"] + a_r
        a_y = RNG.uniform(1.5, 3)
        a_z = center["z"] + a_r
        cam_angle_min, cam_angle_max = scene.get_camera_angles()
        theta = np.radians(RNG.uniform(cam_angle_min, cam_angle_max))
        a_x = np.cos(theta) * (a_x - center["x"]) - np.sin(theta) * (
            a_z - center["z"]) + center["x"]
        a_z = np.sin(theta) * (a_x - center["x"]) + np.cos(theta) * (
            a_z - center["z"]) + center["z"]

        o_id = 0
        # Create the object and apply a force.
        commands = [{
            "$type": "add_object",
            "name": record.name,
            "url": record.get_url(),
            "scale_factor": record.scale_factor,
            "position": {
                "x": o_x,
                "y": o_y,
                "z": o_z
            },
            "category": record.wcategory,
            "id": o_id
        }, {
            "$type": "set_mass",
            "id": o_id,
            "mass": mass
        }, {
            "$type": "set_physic_material",
            "id": o_id,
            "bounciness": self.object_info[record.name].bounciness,
            "static_friction": static_friction,
            "dynamic_friction": dynamic_friction
        }, {
            "$type": "rotate_object_by",
            "angle": yaw,
            "id": o_id,
            "axis": "yaw",
            "is_world": True
        }, {
            "$type": "rotate_object_by",
            "angle": pitch,
            "id": o_id,
            "axis": "pitch",
            "is_world": True
        }, {
            "$type": "rotate_object_by",
            "angle": roll,
            "id": o_id,
            "axis": "roll",
            "is_world": True
        }, {
            "$type": "apply_force_magnitude_to_object",
            "magnitude": force,
            "id": o_id
        }, {
            "$type": "send_rigidbodies",
            "frequency": "always"
        }, {
            "$type": "send_collisions",
            "enter": True,
            "exit": False,
            "stay": False,
            "collision_types": ["obj", "env"]
        }, {
            "$type": "send_transforms",
            "frequency": "always"
        }]
        # Parse bounds data to get the centroid of all objects currently in the scene.
        bounds = Bounds(resp[0])
        if bounds.get_num() == 0:
            look_at = {"x": center["x"], "y": 0.1, "z": center["z"]}
        else:
            centers = []
            for i in range(bounds.get_num()):
                centers.append(bounds.get_center(i))
            centers_x, centers_y, centers_z = zip(*centers)
            centers_len = len(centers_x)
            look_at = {
                "x": sum(centers_x) / centers_len,
                "y": sum(centers_y) / centers_len,
                "z": sum(centers_z) / centers_len
            }
        # Add the avatar.
        # Set the position at a given distance (r) from the center of the scene.
        # Rotate around that position to a random angle constrained by the scene's min and max angles.
        commands.extend([{
            "$type": "teleport_avatar_to",
            "position": {
                "x": a_x,
                "y": a_y,
                "z": a_z
            }
        }, {
            "$type": "look_at_position",
            "position": look_at
        }])

        # Send the commands.
        resp = self.communicate(commands)

        AudioUtils.start(output_path=output_path, until=(0, 10))

        # Loop until all objects are sleeping.
        done = False
        while not done and AudioUtils.is_recording():
            commands = []
            collisions, environment_collisions, rigidbodies = PyImpact.get_collisions(
                resp)
            # Create impact sounds from object-object collisions.
            for collision in collisions:
                if PyImpact.is_valid_collision(collision):
                    # Get the audio material and amp.
                    collider_id = collision.get_collider_id()
                    collider_material, collider_amp = self._get_object_info(
                        collider_id, Scene.OBJECT_IDS, record.name)
                    collidee_id = collision.get_collider_id()
                    collidee_material, collidee_amp = self._get_object_info(
                        collidee_id, Scene.OBJECT_IDS, record.name)
                    impact_sound_command = self.py_impact.get_impact_sound_command(
                        collision=collision,
                        rigidbodies=rigidbodies,
                        target_id=collidee_id,
                        target_amp=collidee_amp,
                        target_mat=collidee_material.name,
                        other_id=collider_id,
                        other_mat=collider_material.name,
                        other_amp=collider_amp,
                        play_audio_data=False)
                    commands.append(impact_sound_command)
            # Create impact sounds from object-environment collisions.
            for collision in environment_collisions:
                collider_id = collision.get_object_id()
                if self._get_velocity(rigidbodies, collider_id) > 0:
                    collider_material, collider_amp = self._get_object_info(
                        collider_id, Scene.OBJECT_IDS, record.name)
                    surface_material = scene.get_surface_material()
                    impact_sound_command = self.py_impact.get_impact_sound_command(
                        collision=collision,
                        rigidbodies=rigidbodies,
                        target_id=collider_id,
                        target_amp=collider_amp,
                        target_mat=collider_material.name,
                        other_id=-1,
                        other_amp=0.01,
                        other_mat=surface_material.name,
                        play_audio_data=False)
                    commands.append(impact_sound_command)
            # If there were no collisions, check for movement. If nothing is moving, the trial is done.
            if len(commands) == 0:
                transforms = AudioDataset._get_transforms(resp)
                done = True
                for i in range(rigidbodies.get_num()):
                    if self._is_moving(rigidbodies.get_id(i), transforms,
                                       rigidbodies):
                        done = False
                        break
            # Continue the trial.
            if not done:
                resp = self.communicate(commands)

        # Stop listening for anything except audio data..
        resp = self.communicate([{
            "$type": "send_rigidbodies",
            "frequency": "never"
        }, {
            "$type": "send_transforms",
            "frequency": "never"
        }, {
            "$type": "send_collisions",
            "enter": False,
            "exit": False,
            "stay": False,
            "collision_types": []
        }, {
            "$type": "send_audio_sources",
            "frequency": "always"
        }])
        # Wait for the audio to finish.
        done = False
        while not done and AudioUtils.is_recording():
            done = True
            for r in resp[:-1]:
                if OutputData.get_data_type_id(r) == "audi":
                    audio_sources = AudioSources(r)
                    for i in range(audio_sources.get_num()):
                        if audio_sources.get_is_playing(i):
                            done = False
            if not done:
                resp = self.communicate([])
        # Cleanup.
        commands = [{
            "$type": "send_audio_sources",
            "frequency": "never"
        }, {
            "$type": "destroy_object",
            "id": o_id
        }]
        for scene_object_id in Scene.OBJECT_IDS:
            commands.append({"$type": "destroy_object", "id": scene_object_id})
        self.communicate(commands)

        # Insert the trial's values into the database.
        self.db_c.execute(
            "INSERT INTO sound20k VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)",
            (output_path.name, scene_index, a_x, a_y, a_z, o_x, o_y, o_z, mass,
             static_friction, dynamic_friction, yaw, pitch, roll, force))
        self.conn.commit()
Example #10
0
                                          "object_id": object_id,
                                          "use_centroid": True}
                                         ])

        grades = [1 - (float(h) / float(i)) for h, i in zip(pink_pass, id_pass)]

        physics_quality = float(sum(grades)) / len(grades)
        print("Physics quality: " + str(physics_quality))

        # Kill the build.
        self.kill_build()
        return physics_quality


if __name__ == "__main__":
    parser = ArgumentParser()
    parser.add_argument("--record_path", type=str, help="The path to the temporary record file")
    parser.add_argument("--asset_bundle_path", type=str, help="The path to the local asset bundle.")
    args = parser.parse_args()

    # Get the physics quality.
    c = PhysicsQualityWriter(record_path=args.record_path,
                             asset_bundle_path=args.asset_bundle_path)
    physics_quality = c.get_physics_quality()

    # Update the record.
    record_path = Path(args.record_path)
    record = ModelRecord(json.loads(record_path.read_text(encoding="utf-8")))
    record.physics_quality = physics_quality
    record_path.write_text(json.dumps(record.__dict__), encoding="utf-8")
Example #11
0
    def process_model(self, record: ModelRecord, a: str, envs: list,
                      train_count: int, val_count: int, root_dir: str,
                      wnid: str) -> float:
        """
        Capture images of a model.

        :param record: The model record.
        :param a: The ID of the avatar.
        :param envs: All environment data.
        :param train_count: Number of train images.
        :param val_count: Number of val images.
        :param root_dir: The root directory for saving images.
        :param wnid: The wnid of the record.
        :return The time elapsed.
        """

        image_count = 0

        # Get the filename index. If we shouldn't overwrite any images, start after the last image.
        if self.no_overwrite:
            # Check if any images exist.
            wnid_dir = Path(root_dir).joinpath(f"train/{wnid}")
            if wnid_dir.exists():
                max_file_index = -1
                for image in wnid_dir.iterdir():
                    if not image.is_file() or image.suffix != ".jpg" \
                            or not image.stem.startswith("img_") or image.stem[4:-5] != record.name:
                        continue
                    image_index = int(image.stem[-4:])
                    if image_index > max_file_index:
                        max_file_index = image_index
                file_index = max_file_index + 1
            else:
                file_index = 0
        else:
            file_index = 0

        image_positions = []
        o_id = self.get_unique_id()

        s = TDWUtils.get_unit_scale(record)

        # Add the object.
        # Set the screen size to 32x32 (to make the build run faster; we only need the average grayscale values).
        # Toggle off pass masks.
        # Set render quality to minimal.
        # Scale the object to "unit size".
        self.communicate([{
            "$type": "add_object",
            "name": record.name,
            "url": record.get_url(),
            "scale_factor": record.scale_factor,
            "category": record.wcategory,
            "id": o_id
        }, {
            "$type": "set_screen_size",
            "height": 32,
            "width": 32
        }, {
            "$type": "set_pass_masks",
            "avatar_id": a,
            "pass_masks": []
        }, {
            "$type": "set_render_quality",
            "render_quality": 0
        }, {
            "$type": "scale_object",
            "id": o_id,
            "scale_factor": {
                "x": s,
                "y": s,
                "z": s
            }
        }])

        # The index in the HDRI records array.
        hdri_index = 0
        # The number of iterations on this skybox so far.
        skybox_count = 0
        if self.skyboxes:
            # The number of iterations per skybox for this model.
            its_per_skybox = round(
                (train_count + val_count) / len(self.skyboxes))

            # Set the first skybox.
            hdri_index, skybox_count, command = self.set_skybox(
                self.skyboxes, its_per_skybox, hdri_index, skybox_count)
            self.communicate(command)
        else:
            its_per_skybox = 0

        while len(image_positions) < train_count + val_count:
            e = RNG.choice(envs)

            # Get the real grayscale.
            g_r, d, a_p, o_p, o_rot, cam_rot = self.get_real_grayscale(
                o_id, a, e)

            if g_r > 0:
                # Get the optimal grayscale.
                g_o = self.get_optimal_grayscale(o_id, a, o_p, a_p)

                if g_o > 0 and g_r / g_o > self.grayscale_threshold:
                    # Cache the position.
                    image_positions.append(
                        ImagePosition(a_p, cam_rot, o_p, o_rot))

        # Send images.
        # Set the screen size.
        # Set render quality to maximum.
        commands = [{
            "$type": "send_images",
            "frequency": "always"
        }, {
            "$type": "set_pass_masks",
            "avatar_id": a,
            "pass_masks": ["_img", "_id"] if self.id_pass else ["_img"]
        }, {
            "$type": "set_screen_size",
            "height": self.screen_size,
            "width": self.screen_size
        }, {
            "$type": "set_render_quality",
            "render_quality": 5
        }]
        # Hide the object maybe.
        if not self.show_objects:
            commands.append({"$type": "hide_object", "id": o_id})

        self.communicate(commands)

        t0 = time()

        # Generate images from the cached spatial data.
        train = 0
        for p in image_positions:
            # Teleport the avatar.
            # Rotate the avatar's camera.
            # Teleport the object.
            # Rotate the object.
            # Get the response.
            commands = [{
                "$type": "teleport_avatar_to",
                "avatar_id": a,
                "position": p.avatar_position
            }, {
                "$type": "rotate_sensor_container_to",
                "avatar_id": a,
                "rotation": p.camera_rotation
            }, {
                "$type": "teleport_object",
                "id": o_id,
                "position": p.object_position
            }, {
                "$type": "rotate_object_to",
                "id": o_id,
                "rotation": p.object_rotation
            }]
            # Set the visual materials.
            if self.materials is not None:
                if record.name not in self.substructures:
                    self.substructures.update(
                        {record.name: record.substructure})
                for sub_object in self.substructures[record.name]:
                    for i in range(
                            len(self.substructures[record.name][
                                sub_object["name"]])):
                        material_name = self.materials[RNG.randint(
                            0, len(self.materials))].name
                        commands.extend([
                            self.get_add_material(material_name), {
                                "$type": "set_visual_material",
                                "id": o_id,
                                "material_name": material_name,
                                "object_name": sub_object["name"],
                                "material_index": i
                            }
                        ])
            # Maybe set a new skybox.
            # Rotate the skybox.
            if self.skyboxes:
                hdri_index, skybox_count, command = self.set_skybox(
                    self.skyboxes, its_per_skybox, hdri_index, skybox_count)
                if command:
                    commands.append(command)
                commands.append({
                    "$type": "rotate_hdri_skybox_by",
                    "angle": RNG.uniform(0, 360)
                })

            resp = self.communicate(commands)
            train += 1

            # Create a thread to save the image.
            t = Thread(target=self.save_image,
                       args=(resp, record, file_index, root_dir, wnid, train,
                             train_count))
            t.daemon = True
            t.start()
            file_index += 1
            image_count += 1
        t1 = time()

        # Stop sending images.
        # Destroy the object.
        # Unload asset bundles.
        self.communicate([{
            "$type": "send_images",
            "frequency": "never"
        }, {
            "$type": "destroy_object",
            "id": o_id
        }, {
            "$type": "unload_asset_bundles"
        }])
        return t1 - t0
    # Create asset bundles.
    a = AssetBundleCreator()
    src_paths = a.prefab_to_asset_bundle(
        Path.home().joinpath("asset_bundle_creator/Assets/Resources/prefab"),
        model_name=args.name)
    src_asset_bundles = dict()
    for q in src_paths:
        src_asset_bundles[q.parts[-2]] = q

    # Parse the URLs.
    urls = a.get_local_urls(src_paths)

    # Create the metadata record.
    record_path = a.create_record(args.name, 2886585, "container", 1, urls)
    record = ModelRecord(json.loads(record_path.read_text(encoding="utf-8")))

    # Add the record.
    r = lib.get_record(record.name)
    lib.add_or_update_record(record=record,
                             overwrite=False if r is None else True,
                             write=True)
    # Make the URLs relative paths.
    temp = dict()
    for p in record.urls:
        dest_dir = f"../asset_bundles/{p}"
        dd = root_dest.joinpath(f"asset_bundles/{p}")
        if not dd.exists():
            dd.mkdir(parents=True)
        temp[p] = f"../asset_bundles/{p}/{record.name}"
    record.urls = temp