Please modify the code to save a single model instead of saving it every epoch, and also make the code save a json file that contains the configuration of the model as well as another txt file that contains the vocabulary of the model, **code**: import torch
import torch.nn as nn
from torch.utils.data import DataLoader, TensorDataset
from tqdm import tqdm
import matplotlib.pyplot as plt
import os

# Define parameters and directory paths
SEQUENCE_LENGTH = 40
BATCH_SIZE = 128
VOCAB_SIZE = 192  # Number of ASCII characters considered in the vocabulary
HIDDEN_SIZE = 384
NUM_LAYERS = 2
LEARNING_RATE = 0.001
NUM_EPOCHS = 10
GRAD_CLIP = 5.0  # Gradient clipping to prevent exploding gradients
CHECKPOINT_FOLDER = "checkpoints"
os.makedirs(CHECKPOINT_FOLDER, exist_ok=True)

# Replace ‘your_file.txt’ with the path to your actual text file.
file_path = "C:/Users/Dell-PC/Desktop/The-Physics-Dataset.txt"

# Read the file and filter out blank lines
with open(file_path, "r", encoding="utf-8") as file:
    lines = file.readlines()
    text = "".join(line for line in lines if not line.isspace())

# Convert text to ASCII values
ascii_characters = [ord(c) for c in text]

# Prepare datasets
inputs = []
targets = []
for i in range(0, len(ascii_characters) - SEQUENCE_LENGTH):
    input_seq = ascii_characters[i:i+SEQUENCE_LENGTH]
    target = ascii_characters[i+1:i+SEQUENCE_LENGTH+1]
    inputs.append(torch.tensor(input_seq, dtype=torch.long))
    targets.append(torch.tensor(target, dtype=torch.long))

input_tensor = torch.stack(inputs)
target_tensor = torch.stack(targets)

# Load data into DataLoader for batch processing
dataset = TensorDataset(input_tensor, target_tensor)
data_loader = DataLoader(dataset, batch_size=BATCH_SIZE, shuffle=True)

# Define the sophisticated RNN model architecture
class AdvancedRNN(nn.Module):
    def __init__(self, vocab_size, hidden_size, num_layers):
        super(AdvancedRNN, self).__init__()
        self.hidden_size = hidden_size
        self.num_layers = num_layers
        self.embedding = nn.Embedding(vocab_size, hidden_size)
        self.rnn = nn.LSTM(hidden_size, hidden_size, num_layers, batch_first=True)
        self.fc = nn.Linear(hidden_size, vocab_size)

    def forward(self, x, hidden):
        x = self.embedding(x)
        out, hidden = self.rnn(x, hidden)
        out = self.fc(out.reshape(out.size(0) * out.size(1), out.size(2)))
        return out, hidden

    def init_hidden(self, batch_size):
        weight = next(self.parameters())
        hidden = (weight.new_zeros(self.num_layers, batch_size, self.hidden_size),
                  weight.new_zeros(self.num_layers, batch_size, self.hidden_size))
        return hidden

# Initialize model, loss function, and optimizer
model = AdvancedRNN(VOCAB_SIZE, HIDDEN_SIZE, NUM_LAYERS)
loss_fn = nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(model.parameters(), lr=LEARNING_RATE)

# Training loop with added production-ready features and tqdm progress
def train_model(model, data_loader, optimizer, loss_fn, epochs, batch_size):
    loss_history = []  # Initialize a list to store average epoch losses
    for epoch in range(epochs):
        model.train()
        hidden = model.init_hidden(batch_size)
        running_loss = 0.0
        # Wrap the data_loader with tqdm for a progress bar
        progress_bar = tqdm(data_loader, desc=f"Epoch {epoch + 1}/{epochs}")

        for input_batch, target_batch in progress_bar:
            if input_batch.size(0) != batch_size:
                hidden = model.init_hidden(input_batch.size(0))

            optimizer.zero_grad()
            output, hidden = model(input_batch, hidden)
            hidden = tuple([h.detach() for h in hidden])  # Detach hidden state
            loss = loss_fn(output, target_batch.view(-1))
            loss.backward()
            
            # Clip gradients to mitigate exploding gradients
            nn.utils.clip_grad_norm_(model.parameters(), GRAD_CLIP)
            optimizer.step()

            running_loss += loss.item()
            progress_bar.set_postfix(loss=loss.item())

        # Calculate and store the average epoch loss
        epoch_loss = running_loss / len(data_loader)  # Change to len(data_loader)
        loss_history.append(epoch_loss)
        print(f"Epoch {epoch + 1}/{epochs}, Loss: {epoch_loss:.4f}")

        # Save the model checkpoint (with enhanced cross-platform directory usage)
        checkpoint_path = os.path.join(CHECKPOINT_FOLDER, f"model_epoch_{epoch + 1}.pth")
        torch.save(model.state_dict(), checkpoint_path)

    # Plot training loss per epoch outside the training loop
    plt.figure()  # Create a new figure
    plt.plot(range(1, epochs + 1), loss_history, label="Training Loss")
    plt.xlabel("Epoch")
    plt.ylabel("Loss")
    plt.legend()
    plt.grid(True)
    plt.title("Training Loss Over Epochs")
    plt.show()

    return model  # Return the trained model

# Predict the next character
def predict(model, char, hidden=None, temperature=1.0):
    # Convert to ASCII and then tensor
    input_tensor = torch.tensor([ord(char)], dtype=torch.long).unsqueeze(0)
    if hidden is None:
        hidden = model.init_hidden(1)
    output, hidden = model(input_tensor, hidden)
    output_dist = output.view(-1).div(temperature).exp()
    top_i = torch.multinomial(output_dist, 1)[0]
    predicted_char = chr(top_i)
    return predicted_char, hidden

# Generate text
def generate_text(model, start_char="P", length=200):
    model.eval()
    hidden = model.init_hidden(1)
    input_char = start_char
    for i in range(length):  
        input_char, hidden = predict(model, input_char, hidden)
        print(input_char, end="")

if __name__ == "__main__":
    # Train the model
    train_model(model, data_loader, optimizer, loss_fn, NUM_EPOCHS, BATCH_SIZE)
    
    # Generate text from the trained model
    generate_text(model, start_char="P", length=200)